Merge pull request #3287 from hashicorp/sethvargo/cli-magic

Standardize all commands with autocomplete, help output, and tests
This commit is contained in:
Jeff Mitchell 2018-01-10 10:58:52 -06:00 committed by GitHub
commit 166db9275e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
390 changed files with 30590 additions and 15071 deletions

View file

@ -293,14 +293,14 @@ IMPROVEMENTS:
(PID) in a file [GH-3321]
* mfa (Enterprise): Add the ability to use identity metadata in username format
* mfa/okta (Enterprise): Add support for configuring base_url for API calls
* secret/pki: `sign-intermediate` will now allow specifying a `ttl` value
* secret/pki: `sign-intermediate` will now allow specifying a `ttl` value
longer than the signing CA certificate's NotAfter value. [GH-3325]
* sys/raw: Raw storage access is now disabled by default [GH-3329]
BUG FIXES:
* auth/okta: Fix regression that removed the ability to set base_url [GH-3313]
* core: Fix panic while loading leases at startup on ARM processors
* core: Fix panic while loading leases at startup on ARM processors
[GH-3314]
* secret/pki: Fix `sign-self-issued` encoding the wrong subject public key
[GH-3325]
@ -350,7 +350,7 @@ IMPROVEMENTS:
* auth/okta: Compare groups case-insensitively since Okta is only
case-preserving [GH-3240]
* auth/okta: Standardize Okta configuration APIs across backends [GH-3245]
* cli: Add subcommand autocompletion that can be enabled with
* cli: Add subcommand autocompletion that can be enabled with
`vault -autocomplete-install` [GH-3223]
* cli: Add ability to handle wrapped responses when using `vault auth`. What
is output depends on the other given flags; see the help output for that

View file

@ -31,7 +31,12 @@ dev-dynamic: prep
# test runs the unit tests and vets the code
test: prep
CGO_ENABLED=0 VAULT_TOKEN= VAULT_ACC= go test -tags='$(BUILD_TAGS)' $(TEST) $(TESTARGS) -timeout=20m -parallel=4
@CGO_ENABLED=0 \
VAULT_ADDR= \
VAULT_TOKEN= \
VAULT_DEV_ROOT_TOKEN_ID= \
VAULT_ACC= \
go test -tags='$(BUILD_TAGS)' $(TEST) $(TESTARGS) -timeout=20m -parallel=20
testcompile: prep
@for pkg in $(TEST) ; do \
@ -48,7 +53,12 @@ testacc: prep
# testrace runs the race checker
testrace: prep
CGO_ENABLED=1 VAULT_TOKEN= VAULT_ACC= go test -tags='$(BUILD_TAGS)' -race $(TEST) $(TESTARGS) -timeout=45m -parallel=4
@CGO_ENABLED=1 \
VAULT_ADDR= \
VAULT_TOKEN= \
VAULT_DEV_ROOT_TOKEN_ID= \
VAULT_ACC= \
go test -tags='$(BUILD_TAGS)' -race $(TEST) $(TESTARGS) -timeout=45m -parallel=20
cover:
./scripts/coverage.sh --html

View file

@ -102,9 +102,9 @@ $ make test TEST=./vault
### Acceptance Tests
Vault has comprehensive [acceptance tests](https://en.wikipedia.org/wiki/Acceptance_testing)
covering most of the features of the secret and auth backends.
covering most of the features of the secret and auth methods.
If you're working on a feature of a secret or auth backend and want to
If you're working on a feature of a secret or auth method and want to
verify it is functioning (and also hasn't broken anything else), we recommend
running the acceptance tests.

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

@ -1,59 +1,131 @@
package api_test
import (
"context"
"database/sql"
"encoding/base64"
"fmt"
"net"
"net/http"
"testing"
"time"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/audit"
"github.com/hashicorp/vault/builtin/logical/database"
"github.com/hashicorp/vault/builtin/logical/pki"
"github.com/hashicorp/vault/builtin/logical/transit"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/vault"
auditFile "github.com/hashicorp/vault/builtin/audit/file"
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
vaulthttp "github.com/hashicorp/vault/http"
logxi "github.com/mgutz/logxi/v1"
dockertest "gopkg.in/ory-am/dockertest.v3"
)
var testVaultServerDefaultBackends = map[string]logical.Factory{
"transit": transit.Factory,
"pki": pki.Factory,
}
// testVaultServer creates a test vault cluster and returns a configured API
// client and closer function.
func testVaultServer(t testing.TB) (*api.Client, func()) {
return testVaultServerBackends(t, testVaultServerDefaultBackends)
t.Helper()
client, _, closer := testVaultServerUnseal(t)
return client, closer
}
func testVaultServerBackends(t testing.TB, backends map[string]logical.Factory) (*api.Client, func()) {
coreConfig := &vault.CoreConfig{
DisableMlock: true,
DisableCache: true,
Logger: logxi.NullLog,
LogicalBackends: backends,
}
// testVaultServerUnseal creates a test vault cluster and returns a configured
// API client, list of unseal keys (as strings), and a closer function.
func testVaultServerUnseal(t testing.TB) (*api.Client, []string, func()) {
t.Helper()
return testVaultServerCoreConfig(t, &vault.CoreConfig{
DisableMlock: true,
DisableCache: true,
Logger: logxi.NullLog,
CredentialBackends: map[string]logical.Factory{
"userpass": credUserpass.Factory,
},
AuditBackends: map[string]audit.Factory{
"file": auditFile.Factory,
},
LogicalBackends: map[string]logical.Factory{
"database": database.Factory,
"generic-leased": vault.LeasedPassthroughBackendFactory,
"pki": pki.Factory,
"transit": transit.Factory,
},
})
}
// testVaultServerCoreConfig creates a new vault cluster with the given core
// configuration. This is a lower-level test helper.
func testVaultServerCoreConfig(t testing.TB, coreConfig *vault.CoreConfig) (*api.Client, []string, func()) {
t.Helper()
cluster := vault.NewTestCluster(t, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
})
cluster.Start()
// make it easy to get access to the active
// Make it easy to get access to the active
core := cluster.Cores[0].Core
vault.TestWaitActive(t, core)
// Get the client already setup for us!
client := cluster.Cores[0].Client
client.SetToken(cluster.RootToken)
// Sanity check
secret, err := client.Auth().Token().LookupSelf()
// Convert the unseal keys to base64 encoded, since these are how the user
// will get them.
unsealKeys := make([]string, len(cluster.BarrierKeys))
for i := range unsealKeys {
unsealKeys[i] = base64.StdEncoding.EncodeToString(cluster.BarrierKeys[i])
}
return client, unsealKeys, func() { defer cluster.Cleanup() }
}
// testVaultServerBad creates an http server that returns a 500 on each request
// to simulate failures.
func testVaultServerBad(t testing.TB) (*api.Client, func()) {
t.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
if secret == nil || secret.Data["id"].(string) != cluster.RootToken {
t.Fatalf("token mismatch: %#v vs %q", secret, cluster.RootToken)
server := &http.Server{
Addr: "127.0.0.1:0",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "500 internal server error", http.StatusInternalServerError)
}),
ReadTimeout: 1 * time.Second,
ReadHeaderTimeout: 1 * time.Second,
WriteTimeout: 1 * time.Second,
IdleTimeout: 1 * time.Second,
}
go func() {
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
t.Fatal(err)
}
}()
client, err := api.NewClient(&api.Config{
Address: "http://" + listener.Addr().String(),
})
if err != nil {
t.Fatal(err)
}
return client, func() {
ctx, done := context.WithTimeout(context.Background(), 5*time.Second)
defer done()
server.Shutdown(ctx)
}
return client, func() { defer cluster.Cleanup() }
}
// testPostgresDB creates a testing postgres database in a Docker container,

View file

@ -5,20 +5,12 @@ import (
"time"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/builtin/logical/database"
"github.com/hashicorp/vault/builtin/logical/pki"
"github.com/hashicorp/vault/builtin/logical/transit"
"github.com/hashicorp/vault/logical"
)
func TestRenewer_Renew(t *testing.T) {
t.Parallel()
client, vaultDone := testVaultServerBackends(t, map[string]logical.Factory{
"database": database.Factory,
"pki": pki.Factory,
"transit": transit.Factory,
})
client, vaultDone := testVaultServer(t)
defer vaultDone()
pgURL, pgDone := testPostgresDB(t)

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,6 +37,188 @@ 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.

File diff suppressed because it is too large Load diff

View file

@ -278,7 +278,7 @@ func getAnyRegionForAwsPartition(partitionId string) *endpoints.Region {
}
const backendHelp = `
aws-ec2 auth backend takes in PKCS#7 signature of an AWS EC2 instance and a client
aws-ec2 auth method takes in PKCS#7 signature of an AWS EC2 instance and a client
created nonce to authenticates the EC2 instance with Vault.
Authentication is backed by a preconfigured role in the backend. The role

View file

@ -113,29 +113,51 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
func (h *CLIHandler) Help() string {
help := `
The AWS credential provider allows you to authenticate with
AWS IAM credentials. To use it, you specify valid AWS IAM credentials
in one of a number of ways. They can be specified explicitly on the
command line (which in general you should not do), via the standard AWS
environment variables (AWS_ACCESS_KEY_ID, AWS_SECRET_ACCESS_KEY, and
AWS_SECURITY_TOKEN), via the ~/.aws/credentials file, or via an EC2
instance profile (in that order).
Usage: vault login -method=aws [CONFIG K=V...]
Example: vault auth -method=aws
The AWS auth method allows users to authenticate with AWS IAM
credentials. The AWS IAM credentials may be specified in a number of ways,
listed in order of precedence below:
If you need to explicitly pass in credentials, you would do it like this:
Example: vault auth -method=aws aws_access_key_id=<access key> aws_secret_access_key=<secret key> aws_security_token=<token>
1. Explicitly via the command line (not recommended)
Key/Value Pairs:
2. Via the standard AWS environment variables (AWS_ACCESS_KEY, etc.)
mount=aws The mountpoint for the AWS credential provider.
Defaults to "aws"
aws_access_key_id=<access key> Explicitly specified AWS access key
aws_secret_access_key=<secret key> Explicitly specified AWS secret key
aws_security_token=<token> Security token for temporary credentials
header_value The Value of the X-Vault-AWS-IAM-Server-ID header.
role The name of the role you're requesting a token for
`
3. Via the ~/.aws/credentials file
4. Via EC2 instance profile
Authenticate using locally stored credentials:
$ vault login -method=aws
Authenticate by passing keys:
$ vault login -method=aws aws_access_key_id=... aws_secret_access_key=...
Configuration:
aws_access_key_id=<string>
Explicit AWS access key ID
aws_secret_access_key=<string>
Explicit AWS secret access key
aws_security_token=<string>
Explicit AWS security token for temporary credentials
header_value=<string>
Value for the x-vault-aws-iam-server-id header in requests
mount=<string>
Path where the AWS credential method is mounted. This is usually provided
via the -path flag in the "vault login" command, but it can be specified
here as well. If specified here, it takes precedence over the value for
-path. The default value is "aws".
role=<string>
Name of the role to request a token against
`
return strings.TrimSpace(help)
}

View file

@ -261,7 +261,7 @@ Configure AWS IAM credentials that are used to query instance and role details f
`
const pathConfigClientHelpDesc = `
The aws-ec2 auth backend makes AWS API queries to retrieve information
The aws-ec2 auth method makes AWS API queries to retrieve information
regarding EC2 instances that perform login operations. The 'aws_secret_key' and
'aws_access_key' parameters configured here should map to an AWS IAM user that
has permission to make the following API queries:

View file

@ -40,17 +40,22 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
func (h *CLIHandler) Help() string {
help := `
The "cert" credential provider allows you to authenticate with a
client certificate. No other authentication materials are needed.
Optionally, you may specify the specific certificate role to
authenticate against with the "name" parameter.
Usage: vault login -method=cert [CONFIG K=V...]
Example: vault auth -method=cert \
-client-cert=/path/to/cert.pem \
-client-key=/path/to/key.pem
name=cert1
The certificate auth method allows uers to authenticate with a
client certificate passed with the request. The -client-cert and -client-key
flags are included with the "vault login" command, NOT as configuration to the
auth method.
`
Authenticate using a local client certificate:
$ vault login -method=cert -client-cert=cert.pem -client-key=key.pem
Configuration:
name=<string>
Certificate role to authenticate against.
`
return strings.TrimSpace(help)
}

View file

@ -2,13 +2,18 @@ package github
import (
"fmt"
"io"
"os"
"strings"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/password"
)
type CLIHandler struct{}
type CLIHandler struct {
// for tests
testStdout io.Writer
}
func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) {
mount, ok := m["mount"]
@ -16,16 +21,39 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
mount = "github"
}
token, ok := m["token"]
if !ok {
if token = os.Getenv("VAULT_AUTH_GITHUB_TOKEN"); token == "" {
return nil, fmt.Errorf("GitHub token should be provided either as 'value' for 'token' key,\nor via an env var VAULT_AUTH_GITHUB_TOKEN")
// Extract or prompt for token
token := m["token"]
if token == "" {
token = os.Getenv("VAULT_AUTH_GITHUB_TOKEN")
}
if token == "" {
// Override the output
stdout := h.testStdout
if stdout == nil {
stdout = os.Stdout
}
var err error
fmt.Fprintf(stdout, "GitHub Personal Access Token (will be hidden): ")
token, err = password.Read(os.Stdin)
fmt.Fprintf(stdout, "\n")
if err != nil {
if err == password.ErrInterrupted {
return nil, fmt.Errorf("user interrupted")
}
return nil, fmt.Errorf("An error occurred attempting to "+
"ask for a token. The raw error message is shown below, but usually "+
"this is because you attempted to pipe a value into the command or "+
"you are executing outside of a terminal (tty). If you want to pipe "+
"the value, pass \"-\" as the argument to read from stdin. The raw "+
"error was: %s", err)
}
}
path := fmt.Sprintf("auth/%s/login", mount)
secret, err := c.Logical().Write(path, map[string]interface{}{
"token": token,
"token": strings.TrimSpace(token),
})
if err != nil {
return nil, err
@ -39,20 +67,28 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
func (h *CLIHandler) Help() string {
help := `
The GitHub credential provider allows you to authenticate with GitHub.
To use it, specify the "token" parameter. The value should be a personal access
token for your GitHub account. You can generate a personal access token on your
account settings page on GitHub.
Usage: vault login -method=github [CONFIG K=V...]
Example: vault auth -method=github token=<token>
The GitHub auth method allows users to authenticate using a GitHub
personal access token. Users can generate a personal access token from the
settings page on their GitHub account.
Key/Value Pairs:
Authenticate using a GitHub token:
mount=github The mountpoint for the GitHub credential provider.
Defaults to "github"
$ vault login -method=github token=abcd1234
token=<token> The GitHub personal access token for authentication.
`
Configuration:
mount=<string>
Path where the GitHub credential method is mounted. This is usually
provided via the -path flag in the "vault login" command, but it can be
specified here as well. If specified here, it takes precedence over the
value for -path. The default value is "github".
token=<string>
GitHub personal access token to use for authentication. If not provided,
Vault will prompt for the value.
`
return strings.TrimSpace(help)
}

View file

@ -103,7 +103,7 @@ func TestLdapAuthBackend_UserPolicies(t *testing.T) {
}
/*
* Acceptance test for LDAP Auth Backend
* Acceptance test for LDAP Auth Method
*
* The tests here rely on a public LDAP server:
* [http://www.forumsys.com/tutorials/integration-how-to/ldap/online-ldap-test-server/]

View file

@ -62,18 +62,40 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
func (h *CLIHandler) Help() string {
help := `
The LDAP credential provider allows you to authenticate with LDAP.
To use it, first configure it through the "config" endpoint, and then
login by specifying username and password. If password is not provided
on the command line, it will be read from stdin.
Usage: vault login -method=ldap [CONFIG K=V...]
If multi-factor authentication (MFA) is enabled, a "method" and/or "passcode"
may be provided depending on the MFA backend enabled. To check
which MFA backend is in use, read "auth/[mount]/mfa_config".
The LDAP auth method allows users to authenticate using LDAP or
Active Directory.
Example: vault auth -method=ldap username=john
If MFA is enabled, a "method" and/or "passcode" may be required depending on
the MFA method. To check which MFA is in use, run:
`
$ vault read auth/<mount>/mfa_config
Authenticate as "sally":
$ vault login -method=ldap username=sally
Password (will be hidden):
Authenticate as "bob":
$ vault login -method=ldap username=bob password=password
Configuration:
method=<string>
MFA method.
passcode=<string>
MFA OTP/passcode.
password=<string>
LDAP password to use for authentication. If not provided, the CLI will
prompt for this on stdin.
username=<string>
LDAP username to use for authentication.
`
return strings.TrimSpace(help)
}

View file

@ -60,7 +60,7 @@ func (b *backend) Login(req *logical.Request, username string, password string)
return nil, nil, nil, err
}
if cfg == nil {
return nil, logical.ErrorResponse("Okta backend not configured"), nil, nil
return nil, logical.ErrorResponse("Okta auth method not configured"), nil, nil
}
client := cfg.OktaClient()
@ -87,7 +87,7 @@ func (b *backend) Login(req *logical.Request, username string, password string)
return nil, logical.ErrorResponse(fmt.Sprintf("Okta auth failed: %v", err)), nil, nil
}
if rsp == nil {
return nil, logical.ErrorResponse("okta auth backend unexpected failure"), nil, nil
return nil, logical.ErrorResponse("okta auth method unexpected failure"), nil, nil
}
oktaResponse := &logical.Response{
@ -161,7 +161,7 @@ func (b *backend) getOktaGroups(client *okta.Client, user *okta.User) ([]string,
return nil, err
}
if rsp == nil {
return nil, fmt.Errorf("okta auth backend unexpected failure")
return nil, fmt.Errorf("okta auth method unexpected failure")
}
oktaGroups := make([]string, 0, len(user.Groups))
for _, group := range user.Groups {

View file

@ -62,14 +62,28 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
// Help method for okta cli
func (h *CLIHandler) Help() string {
help := `
The Okta credential provider allows you to authenticate with Okta.
To use it, first configure it through the "config" endpoint, and then
login by specifying username and password. If password is not provided
on the command line, it will be read from stdin.
Usage: vault login -method=okta [CONFIG K=V...]
Example: vault auth -method=okta username=john
The Okta auth method allows users to authenticate using Okta.
`
Authenticate as "sally":
$ vault login -method=okta username=sally
Password (will be hidden):
Authenticate as "bob":
$ vault login -method=okta username=bob password=password
Configuration:
password=<string>
Okta password to use for authentication. If not provided, the CLI will
prompt for this on stdin.
username=<string>
Okta username to use for authentication.
`
return strings.TrimSpace(help)
}

View file

@ -156,7 +156,7 @@ func (b *backend) pathConfigCreateUpdate(ctx context.Context, req *logical.Reque
policies = strings.Split(unregisteredUserPoliciesStr, ",")
for _, policy := range policies {
if policy == "root" {
return logical.ErrorResponse("root policy cannot be granted by an authentication backend"), nil
return logical.ErrorResponse("root policy cannot be granted by an auth method"), nil
}
}
}

View file

@ -112,7 +112,7 @@ func (b *backend) pathUserWrite(ctx context.Context, req *logical.Request, d *fr
var policies = policyutil.ParsePolicies(d.Get("policies"))
for _, policy := range policies {
if policy == "root" {
return logical.ErrorResponse("root policy cannot be granted by an authentication backend"), nil
return logical.ErrorResponse("root policy cannot be granted by an auth method"), nil
}
}

View file

@ -0,0 +1,166 @@
package token
import (
"fmt"
"io"
"os"
"strconv"
"strings"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/password"
)
type CLIHandler struct {
// for tests
testStdin io.Reader
testStdout io.Writer
}
func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) {
// Parse "lookup" first - we want to return an early error if the user
// supplied an invalid value here before we prompt them for a token. It would
// be annoying to type your token and then be told you supplied an invalid
// value that we could have known in advance.
lookup := true
if x, ok := m["lookup"]; ok {
parsed, err := strconv.ParseBool(x)
if err != nil {
return nil, fmt.Errorf("Failed to parse \"lookup\" as boolean: %s", err)
}
lookup = parsed
}
// Parse the token.
token, ok := m["token"]
if !ok {
// Override the output
stdout := h.testStdout
if stdout == nil {
stdout = os.Stdout
}
// No arguments given, read the token from user input
fmt.Fprintf(stdout, "Token (will be hidden): ")
var err error
token, err = password.Read(os.Stdin)
fmt.Fprintf(stdout, "\n")
if err != nil {
if err == password.ErrInterrupted {
return nil, fmt.Errorf("user interrupted")
}
return nil, fmt.Errorf("An error occurred attempting to "+
"ask for a token. The raw error message is shown below, but usually "+
"this is because you attempted to pipe a value into the command or "+
"you are executing outside of a terminal (tty). If you want to pipe "+
"the value, pass \"-\" as the argument to read from stdin. The raw "+
"error was: %s", err)
}
}
// Remove any whitespace, etc.
token = strings.TrimSpace(token)
if token == "" {
return nil, fmt.Errorf(
"A token must be passed to auth. Please view the help for more " +
"information.")
}
// If the user declined verification, return now. Note that we will not have
// a lot of information about the token.
if !lookup {
return &api.Secret{
Auth: &api.SecretAuth{
ClientToken: token,
},
}, nil
}
// If we got this far, we want to lookup and lookup the token and pull it's
// list of policies an metadata.
c.SetToken(token)
c.SetWrappingLookupFunc(func(string, string) string { return "" })
secret, err := c.Auth().Token().LookupSelf()
if err != nil {
return nil, fmt.Errorf("Error looking up token: %s", err)
}
if secret == nil {
return nil, fmt.Errorf("Empty response from lookup-self")
}
// Return an auth struct that "looks" like the response from an auth method.
// lookup and lookup-self return their data in data, not auth. We try to
// mirror that data here.
id, err := secret.TokenID()
if err != nil {
return nil, fmt.Errorf("Error accessing token ID: %s", err)
}
accessor, err := secret.TokenAccessor()
if err != nil {
return nil, fmt.Errorf("Error accessing token accessor: %s", err)
}
policies, err := secret.TokenPolicies()
if err != nil {
return nil, fmt.Errorf("Error accessing token policies: %s", err)
}
metadata, err := secret.TokenMetadata()
if err != nil {
return nil, fmt.Errorf("Error accessing token metadata: %s", err)
}
dur, err := secret.TokenTTL()
if err != nil {
return nil, fmt.Errorf("Error converting token TTL: %s", err)
}
renewable, err := secret.TokenIsRenewable()
if err != nil {
return nil, fmt.Errorf("Error checking if token is renewable: %s", err)
}
return &api.Secret{
Auth: &api.SecretAuth{
ClientToken: id,
Accessor: accessor,
Policies: policies,
Metadata: metadata,
LeaseDuration: int(dur.Seconds()),
Renewable: renewable,
},
}, nil
}
func (h *CLIHandler) Help() string {
help := `
Usage: vault login TOKEN [CONFIG K=V...]
The token auth method allows logging in directly with a token. This
can be a token from the "token-create" command or API. There are no
configuration options for this auth method.
Authenticate using a token:
$ vault login 96ddf4bc-d217-f3ba-f9bd-017055595017
Authenticate but do not lookup information about the token:
$ vault login token=96ddf4bc-d217-f3ba-f9bd-017055595017 lookup=false
This token usually comes from a different source such as the API or via the
built-in "vault token-create" command.
Configuration:
token=<string>
The token to use for authentication. This is usually provided directly
via the "vault login" command.
lookup=<bool>
Perform a lookup of the token's metadata and policies.
`
return strings.TrimSpace(help)
}

View file

@ -66,20 +66,40 @@ func (h *CLIHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, erro
func (h *CLIHandler) Help() string {
help := `
The "userpass"/"radius" credential provider allows you to authenticate with
a username and password. To use it, specify the "username" and "password"
parameters. If password is not provided on the command line, it will be
read from stdin.
Usage: vault login -method=userpass [CONFIG K=V...]
If multi-factor authentication (MFA) is enabled, a "method" and/or "passcode"
may be provided depending on the MFA backend enabled. To check
which MFA backend is in use, read "auth/[mount]/mfa_config".
The userpass auth method allows users to authenticate using Vault's
internal user database.
Example: vault auth -method=userpass \
username=<user> \
password=<password>
If MFA is enabled, a "method" and/or "passcode" may be required depending on
the MFA method. To check which MFA is in use, run:
`
$ vault read auth/<mount>/mfa_config
Authenticate as "sally":
$ vault login -method=userpass username=sally
Password (will be hidden):
Authenticate as "bob":
$ vault login -method=userpass username=bob password=password
Configuration:
method=<string>
MFA method.
passcode=<string>
MFA OTP/passcode.
password=<string>
Password to use for authentication. If not provided, the CLI will prompt
for this on stdin.
username=<string>
Username to use for authentication.
`
return strings.TrimSpace(help)
}

View file

@ -1,391 +0,0 @@
package cli
import (
"os"
auditFile "github.com/hashicorp/vault/builtin/audit/file"
auditSocket "github.com/hashicorp/vault/builtin/audit/socket"
auditSyslog "github.com/hashicorp/vault/builtin/audit/syslog"
"github.com/hashicorp/vault/physical"
"github.com/hashicorp/vault/version"
credGcp "github.com/hashicorp/vault-plugin-auth-gcp/plugin"
credKube "github.com/hashicorp/vault-plugin-auth-kubernetes"
credAppId "github.com/hashicorp/vault/builtin/credential/app-id"
credAppRole "github.com/hashicorp/vault/builtin/credential/approle"
credAws "github.com/hashicorp/vault/builtin/credential/aws"
credCert "github.com/hashicorp/vault/builtin/credential/cert"
credGitHub "github.com/hashicorp/vault/builtin/credential/github"
credLdap "github.com/hashicorp/vault/builtin/credential/ldap"
credOkta "github.com/hashicorp/vault/builtin/credential/okta"
credRadius "github.com/hashicorp/vault/builtin/credential/radius"
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
physAzure "github.com/hashicorp/vault/physical/azure"
physCassandra "github.com/hashicorp/vault/physical/cassandra"
physCockroachDB "github.com/hashicorp/vault/physical/cockroachdb"
physConsul "github.com/hashicorp/vault/physical/consul"
physCouchDB "github.com/hashicorp/vault/physical/couchdb"
physDynamoDB "github.com/hashicorp/vault/physical/dynamodb"
physEtcd "github.com/hashicorp/vault/physical/etcd"
physFile "github.com/hashicorp/vault/physical/file"
physGCS "github.com/hashicorp/vault/physical/gcs"
physInmem "github.com/hashicorp/vault/physical/inmem"
physMSSQL "github.com/hashicorp/vault/physical/mssql"
physMySQL "github.com/hashicorp/vault/physical/mysql"
physPostgreSQL "github.com/hashicorp/vault/physical/postgresql"
physS3 "github.com/hashicorp/vault/physical/s3"
physSwift "github.com/hashicorp/vault/physical/swift"
physZooKeeper "github.com/hashicorp/vault/physical/zookeeper"
"github.com/hashicorp/vault/builtin/logical/aws"
"github.com/hashicorp/vault/builtin/logical/cassandra"
"github.com/hashicorp/vault/builtin/logical/consul"
"github.com/hashicorp/vault/builtin/logical/database"
"github.com/hashicorp/vault/builtin/logical/mongodb"
"github.com/hashicorp/vault/builtin/logical/mssql"
"github.com/hashicorp/vault/builtin/logical/mysql"
"github.com/hashicorp/vault/builtin/logical/nomad"
"github.com/hashicorp/vault/builtin/logical/pki"
"github.com/hashicorp/vault/builtin/logical/postgresql"
"github.com/hashicorp/vault/builtin/logical/rabbitmq"
"github.com/hashicorp/vault/builtin/logical/ssh"
"github.com/hashicorp/vault/builtin/logical/totp"
"github.com/hashicorp/vault/builtin/logical/transit"
"github.com/hashicorp/vault/builtin/plugin"
"github.com/hashicorp/vault/audit"
"github.com/hashicorp/vault/command"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/meta"
"github.com/mitchellh/cli"
)
// Commands returns the mapping of CLI commands for Vault. The meta
// parameter lets you set meta options for all commands.
func Commands(metaPtr *meta.Meta) map[string]cli.CommandFactory {
if metaPtr == nil {
metaPtr = &meta.Meta{
TokenHelper: command.DefaultTokenHelper,
}
}
if metaPtr.Ui == nil {
metaPtr.Ui = &cli.BasicUi{
Writer: os.Stdout,
ErrorWriter: os.Stderr,
}
}
return map[string]cli.CommandFactory{
"init": func() (cli.Command, error) {
return &command.InitCommand{
Meta: *metaPtr,
}, nil
},
"server": func() (cli.Command, error) {
c := &command.ServerCommand{
Meta: *metaPtr,
AuditBackends: map[string]audit.Factory{
"file": auditFile.Factory,
"syslog": auditSyslog.Factory,
"socket": auditSocket.Factory,
},
CredentialBackends: map[string]logical.Factory{
"approle": credAppRole.Factory,
"cert": credCert.Factory,
"aws": credAws.Factory,
"app-id": credAppId.Factory,
"gcp": credGcp.Factory,
"github": credGitHub.Factory,
"userpass": credUserpass.Factory,
"ldap": credLdap.Factory,
"okta": credOkta.Factory,
"radius": credRadius.Factory,
"kubernetes": credKube.Factory,
"plugin": plugin.Factory,
},
LogicalBackends: map[string]logical.Factory{
"aws": aws.Factory,
"consul": consul.Factory,
"nomad": nomad.Factory,
"postgresql": postgresql.Factory,
"cassandra": cassandra.Factory,
"pki": pki.Factory,
"transit": transit.Factory,
"mongodb": mongodb.Factory,
"mssql": mssql.Factory,
"mysql": mysql.Factory,
"ssh": ssh.Factory,
"rabbitmq": rabbitmq.Factory,
"database": database.Factory,
"totp": totp.Factory,
"plugin": plugin.Factory,
},
ShutdownCh: command.MakeShutdownCh(),
SighupCh: command.MakeSighupCh(),
}
c.PhysicalBackends = map[string]physical.Factory{
"azure": physAzure.NewAzureBackend,
"cassandra": physCassandra.NewCassandraBackend,
"cockroachdb": physCockroachDB.NewCockroachDBBackend,
"consul": physConsul.NewConsulBackend,
"couchdb": physCouchDB.NewCouchDBBackend,
"couchdb_transactional": physCouchDB.NewTransactionalCouchDBBackend,
"dynamodb": physDynamoDB.NewDynamoDBBackend,
"etcd": physEtcd.NewEtcdBackend,
"file": physFile.NewFileBackend,
"file_transactional": physFile.NewTransactionalFileBackend,
"gcs": physGCS.NewGCSBackend,
"inmem": physInmem.NewInmem,
"inmem_ha": physInmem.NewInmemHA,
"inmem_transactional": physInmem.NewTransactionalInmem,
"inmem_transactional_ha": physInmem.NewTransactionalInmemHA,
"mssql": physMSSQL.NewMSSQLBackend,
"mysql": physMySQL.NewMySQLBackend,
"postgresql": physPostgreSQL.NewPostgreSQLBackend,
"s3": physS3.NewS3Backend,
"swift": physSwift.NewSwiftBackend,
"zookeeper": physZooKeeper.NewZooKeeperBackend,
}
return c, nil
},
"ssh": func() (cli.Command, error) {
return &command.SSHCommand{
Meta: *metaPtr,
}, nil
},
"path-help": func() (cli.Command, error) {
return &command.PathHelpCommand{
Meta: *metaPtr,
}, nil
},
"auth": func() (cli.Command, error) {
return &command.AuthCommand{
Meta: *metaPtr,
Handlers: map[string]command.AuthHandler{
"github": &credGitHub.CLIHandler{},
"userpass": &credUserpass.CLIHandler{DefaultMount: "userpass"},
"ldap": &credLdap.CLIHandler{},
"okta": &credOkta.CLIHandler{},
"cert": &credCert.CLIHandler{},
"aws": &credAws.CLIHandler{},
"radius": &credUserpass.CLIHandler{DefaultMount: "radius"},
},
}, nil
},
"auth-enable": func() (cli.Command, error) {
return &command.AuthEnableCommand{
Meta: *metaPtr,
}, nil
},
"auth-disable": func() (cli.Command, error) {
return &command.AuthDisableCommand{
Meta: *metaPtr,
}, nil
},
"audit-list": func() (cli.Command, error) {
return &command.AuditListCommand{
Meta: *metaPtr,
}, nil
},
"audit-disable": func() (cli.Command, error) {
return &command.AuditDisableCommand{
Meta: *metaPtr,
}, nil
},
"audit-enable": func() (cli.Command, error) {
return &command.AuditEnableCommand{
Meta: *metaPtr,
}, nil
},
"key-status": func() (cli.Command, error) {
return &command.KeyStatusCommand{
Meta: *metaPtr,
}, nil
},
"policies": func() (cli.Command, error) {
return &command.PolicyListCommand{
Meta: *metaPtr,
}, nil
},
"policy-delete": func() (cli.Command, error) {
return &command.PolicyDeleteCommand{
Meta: *metaPtr,
}, nil
},
"policy-write": func() (cli.Command, error) {
return &command.PolicyWriteCommand{
Meta: *metaPtr,
}, nil
},
"read": func() (cli.Command, error) {
return &command.ReadCommand{
Meta: *metaPtr,
}, nil
},
"unwrap": func() (cli.Command, error) {
return &command.UnwrapCommand{
Meta: *metaPtr,
}, nil
},
"list": func() (cli.Command, error) {
return &command.ListCommand{
Meta: *metaPtr,
}, nil
},
"write": func() (cli.Command, error) {
return &command.WriteCommand{
Meta: *metaPtr,
}, nil
},
"delete": func() (cli.Command, error) {
return &command.DeleteCommand{
Meta: *metaPtr,
}, nil
},
"rekey": func() (cli.Command, error) {
return &command.RekeyCommand{
Meta: *metaPtr,
}, nil
},
"generate-root": func() (cli.Command, error) {
return &command.GenerateRootCommand{
Meta: *metaPtr,
}, nil
},
"renew": func() (cli.Command, error) {
return &command.RenewCommand{
Meta: *metaPtr,
}, nil
},
"revoke": func() (cli.Command, error) {
return &command.RevokeCommand{
Meta: *metaPtr,
}, nil
},
"seal": func() (cli.Command, error) {
return &command.SealCommand{
Meta: *metaPtr,
}, nil
},
"status": func() (cli.Command, error) {
return &command.StatusCommand{
Meta: *metaPtr,
}, nil
},
"unseal": func() (cli.Command, error) {
return &command.UnsealCommand{
Meta: *metaPtr,
}, nil
},
"step-down": func() (cli.Command, error) {
return &command.StepDownCommand{
Meta: *metaPtr,
}, nil
},
"mount": func() (cli.Command, error) {
return &command.MountCommand{
Meta: *metaPtr,
}, nil
},
"mounts": func() (cli.Command, error) {
return &command.MountsCommand{
Meta: *metaPtr,
}, nil
},
"mount-tune": func() (cli.Command, error) {
return &command.MountTuneCommand{
Meta: *metaPtr,
}, nil
},
"remount": func() (cli.Command, error) {
return &command.RemountCommand{
Meta: *metaPtr,
}, nil
},
"rotate": func() (cli.Command, error) {
return &command.RotateCommand{
Meta: *metaPtr,
}, nil
},
"unmount": func() (cli.Command, error) {
return &command.UnmountCommand{
Meta: *metaPtr,
}, nil
},
"token-create": func() (cli.Command, error) {
return &command.TokenCreateCommand{
Meta: *metaPtr,
}, nil
},
"token-lookup": func() (cli.Command, error) {
return &command.TokenLookupCommand{
Meta: *metaPtr,
}, nil
},
"token-renew": func() (cli.Command, error) {
return &command.TokenRenewCommand{
Meta: *metaPtr,
}, nil
},
"token-revoke": func() (cli.Command, error) {
return &command.TokenRevokeCommand{
Meta: *metaPtr,
}, nil
},
"capabilities": func() (cli.Command, error) {
return &command.CapabilitiesCommand{
Meta: *metaPtr,
}, nil
},
"version": func() (cli.Command, error) {
versionInfo := version.GetVersion()
return &command.VersionCommand{
VersionInfo: versionInfo,
Ui: metaPtr.Ui,
}, nil
},
}
}

View file

@ -1,82 +0,0 @@
package cli
import (
"bytes"
"fmt"
"sort"
"strings"
"github.com/mitchellh/cli"
)
// HelpFunc is a cli.HelpFunc that can is used to output the help for Vault.
func HelpFunc(commands map[string]cli.CommandFactory) string {
commonNames := map[string]struct{}{
"delete": struct{}{},
"path-help": struct{}{},
"read": struct{}{},
"renew": struct{}{},
"revoke": struct{}{},
"write": struct{}{},
"server": struct{}{},
"status": struct{}{},
"unwrap": struct{}{},
}
// Determine the maximum key length, and classify based on type
commonCommands := make(map[string]cli.CommandFactory)
otherCommands := make(map[string]cli.CommandFactory)
maxKeyLen := 0
for key, f := range commands {
if len(key) > maxKeyLen {
maxKeyLen = len(key)
}
if _, ok := commonNames[key]; ok {
commonCommands[key] = f
} else {
otherCommands[key] = f
}
}
var buf bytes.Buffer
buf.WriteString("usage: vault [-version] [-help] <command> [args]\n\n")
buf.WriteString("Common commands:\n")
buf.WriteString(listCommands(commonCommands, maxKeyLen))
buf.WriteString("\nAll other commands:\n")
buf.WriteString(listCommands(otherCommands, maxKeyLen))
return buf.String()
}
// listCommands just lists the commands in the map with the
// given maximum key length.
func listCommands(commands map[string]cli.CommandFactory, maxKeyLen int) string {
var buf bytes.Buffer
// Get the list of keys so we can sort them, and also get the maximum
// key length so they can be aligned properly.
keys := make([]string, 0, len(commands))
for key, _ := range commands {
keys = append(keys, key)
}
sort.Strings(keys)
for _, key := range keys {
commandFunc, ok := commands[key]
if !ok {
// This should never happen since we JUST built the list of
// keys.
panic("command not found: " + key)
}
command, err := commandFunc()
if err != nil {
panic(fmt.Sprintf("command '%s' failed to load: %s", key, err))
}
key = fmt.Sprintf("%s%s", key, strings.Repeat(" ", maxKeyLen-len(key)))
buf.WriteString(fmt.Sprintf(" %s %s\n", key, command.Synopsis()))
}
return buf.String()
}

View file

@ -1,53 +0,0 @@
package cli
import (
"fmt"
"os"
"github.com/mitchellh/cli"
)
func Run(args []string) int {
return RunCustom(args, Commands(nil))
}
func RunCustom(args []string, commands map[string]cli.CommandFactory) int {
// Get the command line args. We shortcut "--version" and "-v" to
// just show the version.
for _, arg := range args {
if arg == "-v" || arg == "-version" || arg == "--version" {
newArgs := make([]string, len(args)+1)
newArgs[0] = "version"
copy(newArgs[1:], args)
args = newArgs
break
}
}
// Build the commands to include in the help now. This is pretty...
// tedious, but we don't have a better way at the moment.
commandsInclude := make([]string, 0, len(commands))
for k, _ := range commands {
switch k {
case "token-disk":
default:
commandsInclude = append(commandsInclude, k)
}
}
cli := &cli.CLI{
Args: args,
Commands: commands,
Name: "vault",
Autocomplete: true,
HelpFunc: cli.FilteredHelpFunc(commandsInclude, HelpFunc),
}
exitCode, err := cli.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err.Error())
return 1
}
return exitCode
}

42
command/audit.go Normal file
View file

@ -0,0 +1,42 @@
package command
import (
"strings"
"github.com/mitchellh/cli"
)
var _ cli.Command = (*AuditCommand)(nil)
type AuditCommand struct {
*BaseCommand
}
func (c *AuditCommand) Synopsis() string {
return "Interact with audit devices"
}
func (c *AuditCommand) Help() string {
helpText := `
Usage: vault audit <subcommand> [options] [args]
This command groups subcommands for interacting with Vault's audit devices.
Users can list, enable, and disable audit devices.
List all enabled audit devices:
$ vault audit list
Enable a new audit device "userpass";
$ vault audit enable file file_path=/var/log/audit.log
Please see the individual subcommand help for detailed usage information.
`
return strings.TrimSpace(helpText)
}
func (c *AuditCommand) Run(args []string) int {
return cli.RunResultHelp
}

View file

@ -4,68 +4,84 @@ import (
"fmt"
"strings"
"github.com/hashicorp/vault/meta"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
// AuditDisableCommand is a Command that mounts a new mount.
var _ cli.Command = (*AuditDisableCommand)(nil)
var _ cli.CommandAutocomplete = (*AuditDisableCommand)(nil)
type AuditDisableCommand struct {
meta.Meta
}
func (c *AuditDisableCommand) Run(args []string) int {
flags := c.Meta.FlagSet("mount", meta.FlagSetDefault)
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) != 1 {
flags.Usage()
c.Ui.Error(fmt.Sprintf(
"\naudit-disable expects one argument: the id to disable"))
return 1
}
id := args[0]
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 2
}
if err := client.Sys().DisableAudit(id); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error disabling audit backend: %s", err))
return 2
}
c.Ui.Output(fmt.Sprintf(
"Successfully disabled audit backend '%s' if it was enabled", id))
return 0
*BaseCommand
}
func (c *AuditDisableCommand) Synopsis() string {
return "Disable an audit backend"
return "Disables an audit device"
}
func (c *AuditDisableCommand) Help() string {
helpText := `
Usage: vault audit-disable [options] id
Usage: vault audit disable [options] PATH
Disable an audit backend.
Disables an audit device. Once an audit device is disabled, no future audit
logs are dispatched to it. The data associated with the audit device is not
affected.
Once the audit backend is disabled no more audit logs will be sent to
it. The data associated with the audit backend isn't affected.
The argument corresponds to the PATH of audit device, not the TYPE!
The "id" parameter should map to the "path" used in "audit-enable". If
no path was provided to "audit-enable" you should use the backend
type (e.g. "file").
Disable the audit device enabled at "file/":
$ vault audit disable file/
` + c.Flags().Help()
General Options:
` + meta.GeneralOptionsUsage()
return strings.TrimSpace(helpText)
}
func (c *AuditDisableCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP)
}
func (c *AuditDisableCommand) AutocompleteArgs() complete.Predictor {
return c.PredictVaultAudits()
}
func (c *AuditDisableCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *AuditDisableCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
path := ensureTrailingSlash(sanitizePath(args[0]))
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
if err := client.Sys().DisableAudit(path); err != nil {
c.UI.Error(fmt.Sprintf("Error disabling audit device: %s", err))
return 2
}
c.UI.Output(fmt.Sprintf("Success! Disabled audit device (if it was enabled) at: %s", path))
return 0
}

View file

@ -1,86 +1,160 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func TestAuditDisable(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
func testAuditDisableCommand(tb testing.TB) (*cli.MockUi, *AuditDisableCommand) {
tb.Helper()
ui := new(cli.MockUi)
c := &AuditDisableCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
ui := cli.NewMockUi()
return ui, &AuditDisableCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
args := []string{
"-address", addr,
"noop",
}
// Run once to get the client
c.Run(args)
// Get the client
client, err := c.Client()
if err != nil {
t.Fatalf("err: %#v", err)
}
if err := client.Sys().EnableAudit("noop", "noop", "", nil); err != nil {
t.Fatalf("err: %#v", err)
}
// Run again
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
func TestAuditDisableWithOptions(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
func TestAuditDisableCommand_Run(t *testing.T) {
t.Parallel()
ui := new(cli.MockUi)
c := &AuditDisableCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
cases := []struct {
name string
args []string
out string
code int
}{
{
"not_enough_args",
nil,
"Not enough arguments",
1,
},
{
"too_many_args",
[]string{"foo", "bar", "baz"},
"Too many arguments",
1,
},
{
"not_real",
[]string{"not_real"},
"Success! Disabled audit device (if it was enabled) at: not_real/",
0,
},
{
"default",
[]string{"file"},
"Success! Disabled audit device (if it was enabled) at: file/",
0,
},
}
args := []string{
"-address", addr,
"noop",
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
if err := client.Sys().EnableAuditWithOptions("file", &api.EnableAuditOptions{
Type: "file",
Options: map[string]string{
"file_path": "discard",
},
}); err != nil {
t.Fatal(err)
}
ui, cmd := testAuditDisableCommand(t)
cmd.client = client
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
// Run once to get the client
c.Run(args)
t.Run("integration", func(t *testing.T) {
t.Parallel()
// Get the client
client, err := c.Client()
if err != nil {
t.Fatalf("err: %#v", err)
}
if err := client.Sys().EnableAuditWithOptions("noop", &api.EnableAuditOptions{
Type: "noop",
Description: "noop",
}); err != nil {
t.Fatalf("err: %#v", err)
}
client, closer := testVaultServer(t)
defer closer()
// Run again
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if err := client.Sys().EnableAuditWithOptions("integration_audit_disable", &api.EnableAuditOptions{
Type: "file",
Options: map[string]string{
"file_path": "discard",
},
}); err != nil {
t.Fatal(err)
}
ui, cmd := testAuditDisableCommand(t)
cmd.client = client
code := cmd.Run([]string{
"integration_audit_disable/",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Success! Disabled audit device (if it was enabled) at: integration_audit_disable/"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
mounts, err := client.Sys().ListMounts()
if err != nil {
t.Fatal(err)
}
if _, ok := mounts["integration_audit_disable"]; ok {
t.Errorf("expected mount to not exist: %#v", mounts)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testAuditDisableCommand(t)
cmd.client = client
code := cmd.Run([]string{
"file",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error disabling audit device: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testAuditDisableCommand(t)
assertNoTabs(t, cmd)
})
}

View file

@ -7,128 +7,85 @@ import (
"strings"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/kv-builder"
"github.com/hashicorp/vault/meta"
"github.com/mitchellh/mapstructure"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
// AuditEnableCommand is a Command that mounts a new mount.
var _ cli.Command = (*AuditEnableCommand)(nil)
var _ cli.CommandAutocomplete = (*AuditEnableCommand)(nil)
type AuditEnableCommand struct {
meta.Meta
*BaseCommand
// A test stdin that can be used for tests
testStdin io.Reader
}
flagDescription string
flagPath string
flagLocal bool
func (c *AuditEnableCommand) Run(args []string) int {
var desc, path string
var local bool
flags := c.Meta.FlagSet("audit-enable", meta.FlagSetDefault)
flags.StringVar(&desc, "description", "", "")
flags.StringVar(&path, "path", "", "")
flags.BoolVar(&local, "local", false, "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) < 1 {
flags.Usage()
c.Ui.Error(fmt.Sprintf(
"\naudit-enable expects at least one argument: the type to enable"))
return 1
}
auditType := args[0]
if path == "" {
path = auditType
}
// Build the options
var stdin io.Reader = os.Stdin
if c.testStdin != nil {
stdin = c.testStdin
}
builder := &kvbuilder.Builder{Stdin: stdin}
if err := builder.Add(args[1:]...); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error parsing options: %s", err))
return 1
}
var opts map[string]string
if err := mapstructure.WeakDecode(builder.Map(), &opts); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error parsing options: %s", err))
return 1
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 1
}
err = client.Sys().EnableAuditWithOptions(path, &api.EnableAuditOptions{
Type: auditType,
Description: desc,
Options: opts,
Local: local,
})
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error enabling audit backend: %s", err))
return 1
}
c.Ui.Output(fmt.Sprintf(
"Successfully enabled audit backend '%s' with path '%s'!", auditType, path))
return 0
testStdin io.Reader // For tests
}
func (c *AuditEnableCommand) Synopsis() string {
return "Enable an audit backend"
return "Enables an audit device"
}
func (c *AuditEnableCommand) Help() string {
helpText := `
Usage: vault audit-enable [options] type [config...]
Usage: vault audit enable [options] TYPE [CONFIG K=V...]
Enable an audit backend.
Enables an audit device at a given path.
This command enables an audit backend of type "type". Additional
options for configuring the audit backend can be specified after the
type in the same format as the "vault write" command in key/value pairs.
This command enables an audit device of TYPE. Additional options for
configuring the audit device can be specified after the type in the same
format as the "vault write" command in key/value pairs.
For example, to configure the file audit backend to write audit logs at
the path /var/log/audit.log:
For example, to configure the file audit device to write audit logs at the
path "/var/log/audit.log":
$ vault audit-enable file file_path=/var/log/audit.log
$ vault audit enable file file_path=/var/log/audit.log
For information on available configuration options, please see the
documentation.
` + c.Flags().Help()
General Options:
` + meta.GeneralOptionsUsage() + `
Audit Enable Options:
-description=<desc> A human-friendly description for the backend. This
shows up only when querying the enabled backends.
-path=<path> Specify a unique path for this audit backend. This
is purely for referencing this audit backend. By
default this will be the backend type.
-local Mark the mount as a local mount. Local mounts
are not replicated nor (if a secondary)
removed by replication.
`
return strings.TrimSpace(helpText)
}
func (c *AuditEnableCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP)
f := set.NewFlagSet("Command Options")
f.StringVar(&StringVar{
Name: "description",
Target: &c.flagDescription,
Default: "",
EnvVar: "",
Completion: complete.PredictAnything,
Usage: "Human-friendly description for the purpose of this audit " +
"device.",
})
f.StringVar(&StringVar{
Name: "path",
Target: &c.flagPath,
Default: "", // The default is complex, so we have to manually document
EnvVar: "",
Completion: complete.PredictAnything,
Usage: "Place where the audit device will be accessible. This must be " +
"unique across all audit devices. This defaults to the \"type\" of the " +
"audit device.",
})
f.BoolVar(&BoolVar{
Name: "local",
Target: &c.flagLocal,
Default: false,
EnvVar: "",
Usage: "Mark the audit device as a local-only device. Local devices " +
"are not replicated or removed by replication.",
})
return set
}
func (c *AuditEnableCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictSet(
"file",
@ -138,9 +95,60 @@ func (c *AuditEnableCommand) AutocompleteArgs() complete.Predictor {
}
func (c *AuditEnableCommand) AutocompleteFlags() complete.Flags {
return complete.Flags{
"-description": complete.PredictNothing,
"-path": complete.PredictNothing,
"-local": complete.PredictNothing,
}
return c.Flags().Completions()
}
func (c *AuditEnableCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
if len(args) < 1 {
c.UI.Error("Missing TYPE!")
return 1
}
// Grab the type
auditType := strings.TrimSpace(args[0])
auditPath := c.flagPath
if auditPath == "" {
auditPath = auditType
}
auditPath = ensureTrailingSlash(auditPath)
// Pull our fake stdin if needed
stdin := (io.Reader)(os.Stdin)
if c.testStdin != nil {
stdin = c.testStdin
}
options, err := parseArgsDataString(stdin, args[1:])
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to parse K=V data: %s", err))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
if err := client.Sys().EnableAuditWithOptions(auditPath, &api.EnableAuditOptions{
Type: auditType,
Description: c.flagDescription,
Options: options,
Local: c.flagLocal,
}); err != nil {
c.UI.Error(fmt.Sprintf("Error enabling audit device: %s", err))
return 2
}
c.UI.Output(fmt.Sprintf("Success! Enabled the %s audit device at: %s", auditType, auditPath))
return 0
}

View file

@ -1,56 +1,160 @@
package command
import (
"reflect"
"strings"
"testing"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func TestAuditEnable(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
func testAuditEnableCommand(tb testing.TB) (*cli.MockUi, *AuditEnableCommand) {
tb.Helper()
ui := new(cli.MockUi)
c := &AuditEnableCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
ui := cli.NewMockUi()
return ui, &AuditEnableCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestAuditEnableCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"empty",
nil,
"Missing TYPE!",
1,
},
{
"not_a_valid_type",
[]string{"nope_definitely_not_a_valid_type_like_ever"},
"",
2,
},
{
"enable",
[]string{"file", "file_path=discard"},
"Success! Enabled the file audit device at: file/",
0,
},
{
"enable_path",
[]string{
"-path", "audit_path",
"file",
"file_path=discard",
},
"Success! Enabled the file audit device at: audit_path/",
0,
},
}
args := []string{
"-address", addr,
"noop",
"foo=bar",
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testAuditEnableCommand(t)
cmd.client = client
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
t.Run("integration", func(t *testing.T) {
t.Parallel()
// Get the client
client, err := c.Client()
if err != nil {
t.Fatalf("err: %#v", err)
}
client, closer := testVaultServer(t)
defer closer()
audits, err := client.Sys().ListAudit()
if err != nil {
t.Fatalf("err: %#v", err)
}
ui, cmd := testAuditEnableCommand(t)
cmd.client = client
audit, ok := audits["noop/"]
if !ok {
t.Fatalf("err: %#v", audits)
}
code := cmd.Run([]string{
"-path", "audit_enable_integration/",
"-description", "The best kind of test",
"file",
"file_path=discard",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := map[string]string{"foo": "bar"}
if !reflect.DeepEqual(audit.Options, expected) {
t.Fatalf("err: %#v", audit)
}
expected := "Success! Enabled the file audit device at: audit_enable_integration/"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
audits, err := client.Sys().ListAudit()
if err != nil {
t.Fatal(err)
}
auditInfo, ok := audits["audit_enable_integration/"]
if !ok {
t.Fatalf("expected audit to exist")
}
if exp := "file"; auditInfo.Type != exp {
t.Errorf("expected %q to be %q", auditInfo.Type, exp)
}
if exp := "The best kind of test"; auditInfo.Description != exp {
t.Errorf("expected %q to be %q", auditInfo.Description, exp)
}
filePath, ok := auditInfo.Options["file_path"]
if !ok || filePath != "discard" {
t.Errorf("missing some options: %#v", auditInfo)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testAuditEnableCommand(t)
cmd.client = client
code := cmd.Run([]string{
"pki",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error enabling audit device: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testAuditEnableCommand(t)
assertNoTabs(t, cmd)
})
}

View file

@ -5,83 +5,158 @@ import (
"sort"
"strings"
"github.com/hashicorp/vault/meta"
"github.com/ryanuber/columnize"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
// AuditListCommand is a Command that lists the enabled audits.
var _ cli.Command = (*AuditListCommand)(nil)
var _ cli.CommandAutocomplete = (*AuditListCommand)(nil)
type AuditListCommand struct {
meta.Meta
*BaseCommand
flagDetailed bool
}
func (c *AuditListCommand) Synopsis() string {
return "Lists enabled audit devices"
}
func (c *AuditListCommand) Help() string {
helpText := `
Usage: vault audit list [options]
Lists the enabled audit devices in the Vault server. The output lists the
enabled audit devices and the options for those devices.
List all audit devices:
$ vault audit list
List detailed output about the audit devices:
$ vault audit list -detailed
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *AuditListCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP)
f := set.NewFlagSet("Command Options")
f.BoolVar(&BoolVar{
Name: "detailed",
Target: &c.flagDetailed,
Default: false,
EnvVar: "",
Usage: "Print detailed information such as options and replication " +
"status about each auth device.",
})
return set
}
func (c *AuditListCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *AuditListCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *AuditListCommand) Run(args []string) int {
flags := c.Meta.FlagSet("audit-list", meta.FlagSetDefault)
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
if len(args) > 0 {
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
c.UI.Error(err.Error())
return 2
}
audits, err := client.Sys().ListAudit()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error reading audits: %s", err))
c.UI.Error(fmt.Sprintf("Error listing audits: %s", err))
return 2
}
if len(audits) == 0 {
c.Ui.Error(fmt.Sprintf(
"No audit backends are enabled. Use `vault audit-enable` to\n" +
"enable an audit backend."))
return 1
c.UI.Output(fmt.Sprintf("No audit devices are enabled."))
return 0
}
if c.flagDetailed {
c.UI.Output(tableOutput(c.detailedAudits(audits), nil))
return 0
}
c.UI.Output(tableOutput(c.simpleAudits(audits), nil))
return 0
}
func (c *AuditListCommand) simpleAudits(audits map[string]*api.Audit) []string {
paths := make([]string, 0, len(audits))
for path, _ := range audits {
paths = append(paths, path)
}
sort.Strings(paths)
columns := []string{"Path | Type | Description | Replication Behavior | Options"}
columns := []string{"Path | Type | Description"}
for _, path := range paths {
audit := audits[path]
columns = append(columns, fmt.Sprintf("%s | %s | %s",
audit.Path,
audit.Type,
audit.Description,
))
}
return columns
}
func (c *AuditListCommand) detailedAudits(audits map[string]*api.Audit) []string {
paths := make([]string, 0, len(audits))
for path, _ := range audits {
paths = append(paths, path)
}
sort.Strings(paths)
columns := []string{"Path | Type | Description | Replication | Options"}
for _, path := range paths {
audit := audits[path]
opts := make([]string, 0, len(audit.Options))
for k, v := range audit.Options {
opts = append(opts, k+"="+v)
}
replicatedBehavior := "replicated"
replication := "replicated"
if audit.Local {
replicatedBehavior = "local"
replication = "local"
}
columns = append(columns, fmt.Sprintf(
"%s | %s | %s | %s | %s", audit.Path, audit.Type, audit.Description, replicatedBehavior, strings.Join(opts, " ")))
columns = append(columns, fmt.Sprintf("%s | %s | %s | %s | %s",
audit.Path,
audit.Type,
audit.Description,
replication,
strings.Join(opts, " "),
))
}
c.Ui.Output(columnize.SimpleFormat(columns))
return 0
}
func (c *AuditListCommand) Synopsis() string {
return "Lists enabled audit backends in Vault"
}
func (c *AuditListCommand) Help() string {
helpText := `
Usage: vault audit-list [options]
List the enabled audit backends.
The output lists the enabled audit backends and the options for those
backends. The options may contain sensitive information, and therefore
only a root Vault user can view this.
General Options:
` + meta.GeneralOptionsUsage()
return strings.TrimSpace(helpText)
return columns
}

View file

@ -1,50 +1,111 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func TestAuditList(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
func testAuditListCommand(tb testing.TB) (*cli.MockUi, *AuditListCommand) {
tb.Helper()
ui := new(cli.MockUi)
c := &AuditListCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
ui := cli.NewMockUi()
return ui, &AuditListCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestAuditListCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"too_many_args",
[]string{"foo"},
"Too many arguments",
1,
},
{
"lists",
nil,
"Path",
0,
},
{
"detailed",
[]string{"-detailed"},
"Options",
0,
},
}
args := []string{
"-address", addr,
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
if err := client.Sys().EnableAuditWithOptions("file", &api.EnableAuditOptions{
Type: "file",
Options: map[string]string{
"file_path": "discard",
},
}); err != nil {
t.Fatal(err)
}
ui, cmd := testAuditListCommand(t)
cmd.client = client
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
// Run once to get the client
c.Run(args)
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
// Get the client
client, err := c.Client()
if err != nil {
t.Fatalf("err: %#v", err)
}
if err := client.Sys().EnableAuditWithOptions("foo", &api.EnableAuditOptions{
Type: "noop",
Description: "noop",
Options: nil,
}); err != nil {
t.Fatalf("err: %#v", err)
}
client, closer := testVaultServerBad(t)
defer closer()
// Run again
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
ui, cmd := testAuditListCommand(t)
cmd.client = client
code := cmd.Run([]string{})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error listing audits: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testAuditListCommand(t)
assertNoTabs(t, cmd)
})
}

View file

@ -1,557 +1,117 @@
package command
import (
"bufio"
"encoding/json"
"fmt"
"flag"
"io"
"os"
"sort"
"strconv"
"io/ioutil"
"strings"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/kv-builder"
"github.com/hashicorp/vault/helper/password"
"github.com/hashicorp/vault/meta"
"github.com/mitchellh/mapstructure"
"github.com/posener/complete"
"github.com/ryanuber/columnize"
"github.com/mitchellh/cli"
)
// AuthHandler is the interface that any auth handlers must implement
// to enable auth via the CLI.
type AuthHandler interface {
Auth(*api.Client, map[string]string) (*api.Secret, error)
Help() string
}
var _ cli.Command = (*AuthCommand)(nil)
// AuthCommand is a Command that handles authentication.
type AuthCommand struct {
meta.Meta
*BaseCommand
Handlers map[string]AuthHandler
Handlers map[string]LoginHandler
// The fields below can be overwritten for tests
testStdin io.Reader
}
func (c *AuthCommand) Run(args []string) int {
var method, authPath string
var methods, methodHelp, noVerify, noStore, tokenOnly bool
flags := c.Meta.FlagSet("auth", meta.FlagSetDefault)
flags.BoolVar(&methods, "methods", false, "")
flags.BoolVar(&methodHelp, "method-help", false, "")
flags.BoolVar(&noVerify, "no-verify", false, "")
flags.BoolVar(&noStore, "no-store", false, "")
flags.BoolVar(&tokenOnly, "token-only", false, "")
flags.StringVar(&method, "method", "", "method")
flags.StringVar(&authPath, "path", "", "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
if methods {
return c.listMethods()
}
args = flags.Args()
tokenHelper, err := c.TokenHelper()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing token helper: %s\n\n"+
"Please verify that the token helper is available and properly\n"+
"configured for your system. Please refer to the documentation\n"+
"on token helpers for more information.",
err))
return 1
}
// token is where the final token will go
handler := c.Handlers[method]
// Read token from stdin if first arg is exactly "-"
var stdin io.Reader = os.Stdin
if c.testStdin != nil {
stdin = c.testStdin
}
if len(args) > 0 && args[0] == "-" {
stdinR := bufio.NewReader(stdin)
args[0], err = stdinR.ReadString('\n')
if err != nil && err != io.EOF {
c.Ui.Error(fmt.Sprintf("Error reading from stdin: %s", err))
return 1
}
args[0] = strings.TrimSpace(args[0])
}
if method == "" {
token := ""
if len(args) > 0 {
token = args[0]
}
handler = &tokenAuthHandler{Token: token}
args = nil
switch authPath {
case "", "auth/token":
default:
c.Ui.Error("Token authentication does not support custom paths")
return 1
}
}
if handler == nil {
methods := make([]string, 0, len(c.Handlers))
for k := range c.Handlers {
methods = append(methods, k)
}
sort.Strings(methods)
c.Ui.Error(fmt.Sprintf(
"Unknown authentication method: %s\n\n"+
"Please use a supported authentication method. The list of supported\n"+
"authentication methods is shown below. Note that this list may not\n"+
"be exhaustive: Vault may support other auth methods. For auth methods\n"+
"unsupported by the CLI, please use the HTTP API.\n\n"+
"%s",
method,
strings.Join(methods, ", ")))
return 1
}
if methodHelp {
c.Ui.Output(handler.Help())
return 0
}
// Warn if the VAULT_TOKEN environment variable is set, as that will take
// precedence. Don't output on token-only since we're likely piping output.
if os.Getenv("VAULT_TOKEN") != "" && !tokenOnly {
c.Ui.Output("==> WARNING: VAULT_TOKEN environment variable set!\n")
c.Ui.Output(" The environment variable takes precedence over the value")
c.Ui.Output(" set by the auth command. Either update the value of the")
c.Ui.Output(" environment variable or unset it to use the new token.\n")
}
var vars map[string]string
if len(args) > 0 {
builder := kvbuilder.Builder{Stdin: os.Stdin}
if err := builder.Add(args...); err != nil {
c.Ui.Error(err.Error())
return 1
}
if err := mapstructure.Decode(builder.Map(), &vars); err != nil {
c.Ui.Error(fmt.Sprintf("Error parsing options: %s", err))
return 1
}
} else {
vars = make(map[string]string)
}
// Build the client so we can auth
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client to auth: %s", err))
return 1
}
if authPath != "" {
vars["mount"] = authPath
}
// Authenticate
secret, err := handler.Auth(client, vars)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
if secret == nil {
c.Ui.Error("Empty response from auth helper")
return 1
}
// If we had requested a wrapped token, we want to unset that request
// before performing further functions
client.SetWrappingLookupFunc(func(string, string) string {
return ""
})
CHECK_TOKEN:
var token string
switch {
case secret == nil:
c.Ui.Error("Empty response from auth helper")
return 1
case secret.Auth != nil:
token = secret.Auth.ClientToken
case secret.WrapInfo != nil:
if secret.WrapInfo.WrappedAccessor == "" {
c.Ui.Error("Got a wrapped response from Vault but wrapped reply does not seem to contain a token")
return 1
}
if tokenOnly {
c.Ui.Output(secret.WrapInfo.Token)
return 0
}
if noStore {
return OutputSecret(c.Ui, "table", secret)
}
client.SetToken(secret.WrapInfo.Token)
secret, err = client.Logical().Unwrap("")
goto CHECK_TOKEN
default:
c.Ui.Error("No auth or wrapping info in auth helper response")
return 1
}
// Cache the previous token so that it can be restored if authentication fails
var previousToken string
if previousToken, err = tokenHelper.Get(); err != nil {
c.Ui.Error(fmt.Sprintf("Error caching the previous token: %s\n\n", err))
return 1
}
if tokenOnly {
c.Ui.Output(token)
return 0
}
// Store the token!
if !noStore {
if err := tokenHelper.Store(token); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error storing token: %s\n\n"+
"Authentication was not successful and did not persist.\n"+
"Please reauthenticate, or fix the issue above if possible.",
err))
return 1
}
}
if noVerify {
c.Ui.Output(fmt.Sprintf(
"Authenticated - no token verification has been performed.",
))
if noStore {
if err := tokenHelper.Erase(); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error removing prior token: %s\n\n"+
"Authentication was successful, but unable to remove the\n"+
"previous token.",
err))
return 1
}
}
return 0
}
// Build the client again so it can read the token we just wrote
client, err = c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client to verify the token: %s", err))
if !noStore {
if err := tokenHelper.Store(previousToken); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error restoring the previous token: %s\n\n"+
"Please reauthenticate with a valid token.",
err))
}
}
return 1
}
client.SetWrappingLookupFunc(func(string, string) string {
return ""
})
// If in no-store mode it won't have read the token from a token-helper (or
// will read an old one) so set it explicitly
if noStore {
client.SetToken(token)
}
// Verify the token
secret, err = client.Auth().Token().LookupSelf()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error validating token: %s", err))
if err := tokenHelper.Store(previousToken); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error restoring the previous token: %s\n\n"+
"Please reauthenticate with a valid token.",
err))
}
return 1
}
if secret == nil && !noStore {
c.Ui.Error(fmt.Sprintf("Error: Invalid token"))
if err := tokenHelper.Store(previousToken); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error restoring the previous token: %s\n\n"+
"Please reauthenticate with a valid token.",
err))
}
return 1
}
if noStore {
if err := tokenHelper.Erase(); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error removing prior token: %s\n\n"+
"Authentication was successful, but unable to remove the\n"+
"previous token.",
err))
return 1
}
}
// Get the policies we have
policiesRaw, ok := secret.Data["policies"]
if !ok || policiesRaw == nil {
policiesRaw = []interface{}{"unknown"}
}
var policies []string
for _, v := range policiesRaw.([]interface{}) {
policies = append(policies, v.(string))
}
output := "Successfully authenticated! You are now logged in."
if noStore {
output += "\nThe token has not been stored to the configured token helper."
}
if method != "" {
output += "\nThe token below is already saved in the session. You do not"
output += "\nneed to \"vault auth\" again with the token."
}
output += fmt.Sprintf("\ntoken: %s", secret.Data["id"])
output += fmt.Sprintf("\ntoken_duration: %s", secret.Data["ttl"].(json.Number).String())
if len(policies) > 0 {
output += fmt.Sprintf("\ntoken_policies: %v", policies)
}
c.Ui.Output(output)
return 0
}
func (c *AuthCommand) getMethods() (map[string]*api.AuthMount, error) {
client, err := c.Client()
if err != nil {
return nil, err
}
client.SetWrappingLookupFunc(func(string, string) string {
return ""
})
auth, err := client.Sys().ListAuth()
if err != nil {
return nil, err
}
return auth, nil
}
func (c *AuthCommand) listMethods() int {
auth, err := c.getMethods()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error reading auth table: %s", err))
return 1
}
paths := make([]string, 0, len(auth))
for path := range auth {
paths = append(paths, path)
}
sort.Strings(paths)
columns := []string{"Path | Type | Accessor | Default TTL | Max TTL | Replication Behavior | Seal Wrap | Description"}
for _, path := range paths {
auth := auth[path]
defTTL := "system"
if auth.Config.DefaultLeaseTTL != 0 {
defTTL = strconv.Itoa(auth.Config.DefaultLeaseTTL)
}
maxTTL := "system"
if auth.Config.MaxLeaseTTL != 0 {
maxTTL = strconv.Itoa(auth.Config.MaxLeaseTTL)
}
replicatedBehavior := "replicated"
if auth.Local {
replicatedBehavior = "local"
}
columns = append(columns, fmt.Sprintf(
"%s | %s | %s | %s | %s | %s | %t | %s", path, auth.Type, auth.Accessor, defTTL, maxTTL, replicatedBehavior, auth.SealWrap, auth.Description))
}
c.Ui.Output(columnize.SimpleFormat(columns))
return 0
testStdin io.Reader // for tests
}
func (c *AuthCommand) Synopsis() string {
return "Prints information about how to authenticate with Vault"
return "Interact with auth methods"
}
func (c *AuthCommand) Help() string {
helpText := `
Usage: vault auth [options] [auth-information]
return strings.TrimSpace(`
Usage: vault auth <subcommand> [options] [args]
Authenticate with Vault using the given token or via any supported
authentication backend.
This command groups subcommands for interacting with Vault's auth methods.
Users can list, enable, disable, and get help for different auth methods.
By default, the -method is assumed to be token. If not supplied via the
command-line, a prompt for input will be shown. If the authentication
information is "-", it will be read from stdin.
To authenticate to Vault as a user or machine, use the "vault login" command
instead. This command is for interacting with the auth methods themselves, not
authenticating to Vault.
The -method option allows alternative authentication methods to be used,
such as userpass, GitHub, or TLS certificates. For these, additional
values as "key=value" pairs may be required. For example, to authenticate
to the userpass auth backend:
List all enabled auth methods:
$ vault auth -method=userpass username=my-username
$ vault auth list
Use "-method-help" to get help for a specific method.
Enable a new auth method "userpass";
If an auth backend is enabled at a different path, the "-method" flag
should still point to the canonical name, and the "-path" flag should be
used. If a GitHub auth backend was mounted as "github-private", one would
authenticate to this backend via:
$ vault auth enable userpass
$ vault auth -method=github -path=github-private
Get detailed help information about how to authenticate to a particular auth
method:
The value of the "-path" flag is supplied to auth providers as the "mount"
option in the payload to specify the mount point.
$ vault auth help github
If response wrapping is used (via -wrap-ttl), the returned token will be
automatically unwrapped unless:
* -token-only is used, in which case the wrapping token will be output
* -no-store is used, in which case the details of the wrapping token
will be printed
General Options:
` + meta.GeneralOptionsUsage() + `
Auth Options:
-method=name Use the method given here, which is a type of backend, not
the path. If this authentication method is not available,
exit with code 1.
-method-help If set, the help for the selected method will be shown.
-methods List the available auth methods.
-no-verify Do not verify the token after creation; avoids a use count
decrement.
-no-store Do not store the token after creation; it will only be
displayed in the command output.
-token-only Output only the token to stdout. This implies -no-verify
and -no-store.
-path The path at which the auth backend is enabled. If an auth
backend is mounted at multiple paths, this option can be
used to authenticate against specific paths.
`
return strings.TrimSpace(helpText)
Please see the individual subcommand help for detailed usage information.
`)
}
// tokenAuthHandler handles retrieving the token from the command-line.
type tokenAuthHandler struct {
Token string
}
func (c *AuthCommand) Run(args []string) int {
// If we entered the run method, none of the subcommands picked up. This
// means the user is still trying to use auth as "vault auth TOKEN" or
// similar, so direct them to vault login instead.
//
// This run command is a bit messy to maintain BC for a bit. In the future,
// it will just be a tiny function, but for now we have to maintain bc.
//
// Deprecation
// TODO: remove in 0.9.0
func (h *tokenAuthHandler) Auth(*api.Client, map[string]string) (*api.Secret, error) {
token := h.Token
if token == "" {
var err error
// Parse the args for our deprecations and defer to the proper areas.
for _, arg := range args {
switch {
case strings.HasPrefix(arg, "-methods"):
c.UI.Warn(wrapAtLength(
"WARNING! The -methods flag is deprecated. Please use "+
"\"vault auth list\" instead. This flag will be removed in the "+
"next major release of Vault.") + "\n")
return (&AuthListCommand{
BaseCommand: &BaseCommand{
UI: c.UI,
client: c.client,
},
}).Run(nil)
case strings.HasPrefix(arg, "-method-help"):
c.UI.Warn(wrapAtLength(
"WARNING! The -method-help flag is deprecated. Please use "+
"\"vault auth help\" instead. This flag will be removed in the "+
"next major release of Vault.") + "\n")
// Parse the args to pull out the method, surpressing any errors because
// there could be other flags that we don't care about.
f := flag.NewFlagSet("", flag.ContinueOnError)
f.Usage = func() {}
f.SetOutput(ioutil.Discard)
flagMethod := f.String("method", "", "")
f.Parse(args)
// No arguments given, read the token from user input
fmt.Printf("Token (will be hidden): ")
token, err = password.Read(os.Stdin)
fmt.Printf("\n")
if err != nil {
return nil, fmt.Errorf(
"Error attempting to ask for token. The raw error message\n"+
"is shown below, but the most common reason for this error is\n"+
"that you attempted to pipe a value into auth. If you want to\n"+
"pipe the token, please pass '-' as the token argument.\n\n"+
"Raw error: %s", err)
return (&AuthHelpCommand{
BaseCommand: &BaseCommand{
UI: c.UI,
client: c.client,
},
Handlers: c.Handlers,
}).Run([]string{*flagMethod})
}
}
if token == "" {
return nil, fmt.Errorf(
"A token must be passed to auth. Please view the help\n" +
"for more information.")
}
return &api.Secret{
Auth: &api.SecretAuth{
ClientToken: token,
// If we got this far, we have an arg or a series of args that should be
// passed directly to the new "vault login" command.
c.UI.Warn(wrapAtLength(
"WARNING! The \"vault auth ARG\" command is deprecated and is now a "+
"subcommand for interacting with auth methods. To "+
"authenticate locally to Vault, use \"vault login\" instead. This "+
"backwards compatability will be removed in the next major release of "+
"Vault.") + "\n")
return (&LoginCommand{
BaseCommand: &BaseCommand{
UI: c.UI,
client: c.client,
},
}, nil
}
func (h *tokenAuthHandler) Help() string {
help := `
No method selected with the "-method" flag, so the "auth" command assumes
you'll be using raw token authentication. For this, specify the token to
authenticate as the parameter to "vault auth". Example:
vault auth 123456
The token used to authenticate must come from some other source. A root
token is created when Vault is first initialized. After that, subsequent
tokens are created via the API or command line interface (with the
"token"-prefixed commands).
`
return strings.TrimSpace(help)
}
func (c *AuthCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictNothing
}
func (c *AuthCommand) AutocompleteFlags() complete.Flags {
var predictFunc complete.PredictFunc = func(a complete.Args) []string {
auths, err := c.getMethods()
if err != nil {
return []string{}
}
methods := make([]string, 0, len(auths))
for _, auth := range auths {
if strings.HasPrefix(auth.Type, a.Last) {
methods = append(methods, auth.Type)
}
}
return methods
}
return complete.Flags{
"-method": predictFunc,
"-methods": complete.PredictNothing,
"-method-help": complete.PredictNothing,
"-no-verify": complete.PredictNothing,
"-no-store": complete.PredictNothing,
"-token-only": complete.PredictNothing,
"-path": complete.PredictNothing,
}
Handlers: c.Handlers,
}).Run(args)
}

View file

@ -4,66 +4,84 @@ import (
"fmt"
"strings"
"github.com/hashicorp/vault/meta"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
// AuthDisableCommand is a Command that enables a new endpoint.
var _ cli.Command = (*AuthDisableCommand)(nil)
var _ cli.CommandAutocomplete = (*AuthDisableCommand)(nil)
type AuthDisableCommand struct {
meta.Meta
}
func (c *AuthDisableCommand) Run(args []string) int {
flags := c.Meta.FlagSet("auth-disable", meta.FlagSetDefault)
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) != 1 {
flags.Usage()
c.Ui.Error(fmt.Sprintf(
"\nauth-disable expects one argument: the path to disable."))
return 1
}
path := args[0]
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 2
}
if err := client.Sys().DisableAuth(path); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error: %s", err))
return 2
}
c.Ui.Output(fmt.Sprintf(
"Disabled auth provider at path '%s' if it was enabled", path))
return 0
*BaseCommand
}
func (c *AuthDisableCommand) Synopsis() string {
return "Disable an auth provider"
return "Disables an auth method"
}
func (c *AuthDisableCommand) Help() string {
helpText := `
Usage: vault auth-disable [options] path
Usage: vault auth disable [options] PATH
Disable an already-enabled auth provider.
Disables an existing auth method at the given PATH. The argument corresponds
to the PATH of the mount, not the TYPE!. Once the auth method is disabled its
path can no longer be used to authenticate.
Once the auth provider is disabled its path can no longer be used
to authenticate. All access tokens generated via the disabled auth provider
will be revoked. This command will block until all tokens are revoked.
If the command is exited early the tokens will still be revoked.
All access tokens generated via the disabled auth method are immediately
revoked. This command will block until all tokens are revoked.
Disable the auth method at userpass/:
$ vault auth disable userpass/
` + c.Flags().Help()
General Options:
` + meta.GeneralOptionsUsage()
return strings.TrimSpace(helpText)
}
func (c *AuthDisableCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP)
}
func (c *AuthDisableCommand) AutocompleteArgs() complete.Predictor {
return c.PredictVaultAuths()
}
func (c *AuthDisableCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *AuthDisableCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
path := ensureTrailingSlash(sanitizePath(args[0]))
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
if err := client.Sys().DisableAuth(path); err != nil {
c.UI.Error(fmt.Sprintf("Error disabling auth method at %s: %s", path, err))
return 2
}
c.UI.Output(fmt.Sprintf("Success! Disabled the auth method (if it existed) at: %s", path))
return 0
}

View file

@ -1,102 +1,133 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func TestAuthDisable(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
func testAuthDisableCommand(tb testing.TB) (*cli.MockUi, *AuthDisableCommand) {
tb.Helper()
ui := new(cli.MockUi)
c := &AuthDisableCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
ui := cli.NewMockUi()
return ui, &AuthDisableCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
args := []string{
"-address", addr,
"noop",
}
// Run the command once to setup the client, it will fail
c.Run(args)
client, err := c.Client()
if err != nil {
t.Fatalf("err: %s", err)
}
if err := client.Sys().EnableAuth("noop", "noop", ""); err != nil {
t.Fatalf("err: %s", err)
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
mounts, err := client.Sys().ListAuth()
if err != nil {
t.Fatalf("err: %s", err)
}
if _, ok := mounts["noop"]; ok {
t.Fatal("should not have noop mount")
}
}
func TestAuthDisableWithOptions(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
func TestAuthDisableCommand_Run(t *testing.T) {
t.Parallel()
ui := new(cli.MockUi)
c := &AuthDisableCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
cases := []struct {
name string
args []string
out string
code int
}{
{
"not_enough_args",
nil,
"Not enough arguments",
1,
},
{
"too_many_args",
[]string{"foo", "bar"},
"Too many arguments",
1,
},
}
args := []string{
"-address", addr,
"noop",
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
// Run the command once to setup the client, it will fail
c.Run(args)
for _, tc := range cases {
tc := tc
client, err := c.Client()
if err != nil {
t.Fatalf("err: %s", err)
}
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
if err := client.Sys().EnableAuthWithOptions("noop", &api.EnableAuthOptions{
Type: "noop",
Description: "",
}); err != nil {
t.Fatalf("err: %#v", err)
}
ui, cmd := testAuthDisableCommand(t)
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
mounts, err := client.Sys().ListAuth()
if err != nil {
t.Fatalf("err: %s", err)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
if _, ok := mounts["noop"]; ok {
t.Fatal("should not have noop mount")
}
t.Run("integration", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
if err := client.Sys().EnableAuth("my-auth", "userpass", ""); err != nil {
t.Fatal(err)
}
ui, cmd := testAuthDisableCommand(t)
cmd.client = client
code := cmd.Run([]string{
"my-auth",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Success! Disabled the auth method"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
auths, err := client.Sys().ListAuth()
if err != nil {
t.Fatal(err)
}
if auth, ok := auths["my-auth/"]; ok {
t.Errorf("expected auth to be disabled: %#v", auth)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testAuthDisableCommand(t)
cmd.client = client
code := cmd.Run([]string{
"my-auth",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error disabling auth method at my-auth/: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testAuthDisableCommand(t)
assertNoTabs(t, cmd)
})
}

View file

@ -5,142 +5,168 @@ import (
"strings"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/meta"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
// AuthEnableCommand is a Command that enables a new endpoint.
var _ cli.Command = (*AuthEnableCommand)(nil)
var _ cli.CommandAutocomplete = (*AuthEnableCommand)(nil)
type AuthEnableCommand struct {
meta.Meta
}
*BaseCommand
func (c *AuthEnableCommand) Run(args []string) int {
var description, path, pluginName string
var local, sealWrap bool
flags := c.Meta.FlagSet("auth-enable", meta.FlagSetDefault)
flags.StringVar(&description, "description", "", "")
flags.StringVar(&path, "path", "", "")
flags.StringVar(&pluginName, "plugin-name", "", "")
flags.BoolVar(&local, "local", false, "")
flags.BoolVar(&sealWrap, "seal-wrap", false, "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) != 1 {
flags.Usage()
c.Ui.Error(fmt.Sprintf(
"\nauth-enable expects one argument: the type to enable."))
return 1
}
authType := args[0]
// If no path is specified, we default the path to the backend type
// or use the plugin name if it's a plugin backend
if path == "" {
if authType == "plugin" {
path = pluginName
} else {
path = authType
}
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 2
}
if err := client.Sys().EnableAuthWithOptions(path, &api.EnableAuthOptions{
Type: authType,
Description: description,
Config: api.AuthConfigInput{
PluginName: pluginName,
},
Local: local,
SealWrap: sealWrap,
}); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error: %s", err))
return 2
}
authTypeOutput := fmt.Sprintf("'%s'", authType)
if authType == "plugin" {
authTypeOutput = fmt.Sprintf("plugin '%s'", pluginName)
}
c.Ui.Output(fmt.Sprintf(
"Successfully enabled %s at '%s'!",
authTypeOutput, path))
return 0
flagDescription string
flagPath string
flagPluginName string
flagLocal bool
flagSealWrap bool
}
func (c *AuthEnableCommand) Synopsis() string {
return "Enable a new auth provider"
return "Enables a new auth method"
}
func (c *AuthEnableCommand) Help() string {
helpText := `
Usage: vault auth-enable [options] type
Usage: vault auth enable [options] TYPE
Enable a new auth provider.
Enables a new auth method. An auth method is responsible for authenticating
users or machines and assigning them policies with which they can access
Vault.
This command enables a new auth provider. An auth provider is responsible
for authenticating a user and assigning them policies with which they can
access Vault.
Enable the userpass auth method at userpass/:
General Options:
` + meta.GeneralOptionsUsage() + `
Auth Enable Options:
$ vault auth enable userpass
-description=<desc> Human-friendly description of the purpose of the
auth provider. This shows up in the auth -methods command.
Enable the LDAP auth method at auth-prod/:
-path=<path> Mount point for the auth provider. This defaults
to the type of the mount. This will make the auth
provider available at "/auth/<path>"
$ vault auth enable -path=auth-prod ldap
-plugin-name Name of the auth plugin to use based from the name
in the plugin catalog.
Enable a custom auth plugin (after it's registered in the plugin registry):
-local Mark the mount as a local mount. Local mounts
are not replicated nor (if a secondary)
removed by replication.
$ vault auth enable -path=my-auth -plugin-name=my-auth-plugin plugin
` + c.Flags().Help()
-seal-wrap Turn on seal wrapping for the mount.
`
return strings.TrimSpace(helpText)
}
func (c *AuthEnableCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictSet(
"approle",
"cert",
"aws",
"app-id",
"gcp",
"github",
"userpass",
"ldap",
"okta",
"radius",
"plugin",
)
func (c *AuthEnableCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP)
f := set.NewFlagSet("Command Options")
f.StringVar(&StringVar{
Name: "description",
Target: &c.flagDescription,
Completion: complete.PredictAnything,
Usage: "Human-friendly description for the purpose of this " +
"auth method.",
})
f.StringVar(&StringVar{
Name: "path",
Target: &c.flagPath,
Default: "", // The default is complex, so we have to manually document
Completion: complete.PredictAnything,
Usage: "Place where the auth method will be accessible. This must be " +
"unique across all auth methods. This defaults to the \"type\" of " +
"the auth method. The auth method will be accessible at " +
"\"/auth/<path>\".",
})
f.StringVar(&StringVar{
Name: "plugin-name",
Target: &c.flagPluginName,
Completion: complete.PredictAnything,
Usage: "Name of the auth method plugin. This plugin name must already " +
"exist in the Vault server's plugin catalog.",
})
f.BoolVar(&BoolVar{
Name: "local",
Target: &c.flagLocal,
Default: false,
Usage: "Mark the auth method as local-only. Local auth methods are " +
"not replicated nor removed by replication.",
})
f.BoolVar(&BoolVar{
Name: "seal-wrap",
Target: &c.flagSealWrap,
Default: false,
Usage: "Enable seal wrapping of critical values in the secrets engine.",
})
return set
}
func (c *AuthEnableCommand) AutocompleteArgs() complete.Predictor {
return c.PredictVaultAvailableAuths()
}
func (c *AuthEnableCommand) AutocompleteFlags() complete.Flags {
return complete.Flags{
"-description": complete.PredictNothing,
"-path": complete.PredictNothing,
"-plugin-name": complete.PredictNothing,
"-local": complete.PredictNothing,
"-seal-wrap": complete.PredictNothing,
}
return c.Flags().Completions()
}
func (c *AuthEnableCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
authType := strings.TrimSpace(args[0])
// If no path is specified, we default the path to the backend type
// or use the plugin name if it's a plugin backend
authPath := c.flagPath
if authPath == "" {
if authType == "plugin" {
authPath = c.flagPluginName
} else {
authPath = authType
}
}
// Append a trailing slash to indicate it's a path in output
authPath = ensureTrailingSlash(authPath)
if err := client.Sys().EnableAuthWithOptions(authPath, &api.EnableAuthOptions{
Type: authType,
Description: c.flagDescription,
Local: c.flagLocal,
SealWrap: c.flagSealWrap,
Config: api.AuthConfigInput{
PluginName: c.flagPluginName,
},
}); err != nil {
c.UI.Error(fmt.Sprintf("Error enabling %s auth: %s", authType, err))
return 2
}
authThing := authType + " auth method"
if authType == "plugin" {
authThing = c.flagPluginName + " plugin"
}
c.UI.Output(fmt.Sprintf("Success! Enabled %s at: %s", authThing, authPath))
return 0
}

View file

@ -1,50 +1,144 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func TestAuthEnable(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
func testAuthEnableCommand(tb testing.TB) (*cli.MockUi, *AuthEnableCommand) {
tb.Helper()
ui := new(cli.MockUi)
c := &AuthEnableCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
ui := cli.NewMockUi()
return ui, &AuthEnableCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestAuthEnableCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"not_enough_args",
nil,
"Not enough arguments",
1,
},
{
"too_many_args",
[]string{"foo", "bar"},
"Too many arguments",
1,
},
{
"not_a_valid_auth",
[]string{"nope_definitely_not_a_valid_mount_like_ever"},
"",
2,
},
}
args := []string{
"-address", addr,
"noop",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testAuthEnableCommand(t)
cmd.client = client
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
client, err := c.Client()
if err != nil {
t.Fatalf("err: %s", err)
}
t.Run("integration", func(t *testing.T) {
t.Parallel()
mounts, err := client.Sys().ListAuth()
if err != nil {
t.Fatalf("err: %s", err)
}
client, closer := testVaultServer(t)
defer closer()
mount, ok := mounts["noop/"]
if !ok {
t.Fatal("should have noop mount")
}
if mount.Type != "noop" {
t.Fatal("should be noop type")
}
ui, cmd := testAuthEnableCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-path", "auth_integration/",
"-description", "The best kind of test",
"userpass",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Success! Enabled userpass auth method at:"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
auths, err := client.Sys().ListAuth()
if err != nil {
t.Fatal(err)
}
authInfo, ok := auths["auth_integration/"]
if !ok {
t.Fatalf("expected mount to exist")
}
if exp := "userpass"; authInfo.Type != exp {
t.Errorf("expected %q to be %q", authInfo.Type, exp)
}
if exp := "The best kind of test"; authInfo.Description != exp {
t.Errorf("expected %q to be %q", authInfo.Description, exp)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testAuthEnableCommand(t)
cmd.client = client
code := cmd.Run([]string{
"userpass",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error enabling userpass auth: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testAuthEnableCommand(t)
assertNoTabs(t, cmd)
})
}

125
command/auth_help.go Normal file
View file

@ -0,0 +1,125 @@
package command
import (
"fmt"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*AuthHelpCommand)(nil)
var _ cli.CommandAutocomplete = (*AuthHelpCommand)(nil)
type AuthHelpCommand struct {
*BaseCommand
Handlers map[string]LoginHandler
}
func (c *AuthHelpCommand) Synopsis() string {
return "Prints usage for an auth method"
}
func (c *AuthHelpCommand) Help() string {
helpText := `
Usage: vault auth help [options] TYPE | PATH
Prints usage and help for an auth method.
- If given a TYPE, this command prints the default help for the
auth method of that type.
- If given a PATH, this command prints the help output for the
auth method enabled at that path. This path must already
exist.
Get usage instructions for the userpass auth method:
$ vault auth help userpass
Print usage for the auth method enabled at my-method/:
$ vault auth help my-method/
Each auth method produces its own help output.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *AuthHelpCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP)
}
func (c *AuthHelpCommand) AutocompleteArgs() complete.Predictor {
handlers := make([]string, 0, len(c.Handlers))
for k := range c.Handlers {
handlers = append(handlers, k)
}
return complete.PredictSet(handlers...)
}
func (c *AuthHelpCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *AuthHelpCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
// Start with the assumption that we have an auth type, not a path.
authType := strings.TrimSpace(args[0])
authHandler, ok := c.Handlers[authType]
if !ok {
// There was no auth type by that name, see if it's a mount
auths, err := client.Sys().ListAuth()
if err != nil {
c.UI.Error(fmt.Sprintf("Error listing auth methods: %s", err))
return 2
}
authPath := ensureTrailingSlash(sanitizePath(args[0]))
auth, ok := auths[authPath]
if !ok {
c.UI.Error(fmt.Sprintf(
"Error retrieving help: unknown auth method: %s", authType))
return 1
}
authHandler, ok = c.Handlers[auth.Type]
if !ok {
c.UI.Error(wrapAtLength(fmt.Sprintf(
"INTERNAL ERROR! Found an auth method enabled at %s, but "+
"its type %q is not registered in Vault. This is a bug and should "+
"be reported. Please open an issue at github.com/hashicorp/vault.",
authPath, authType)))
return 2
}
}
c.UI.Output(authHandler.Help())
return 0
}

152
command/auth_help_test.go Normal file
View file

@ -0,0 +1,152 @@
package command
import (
"strings"
"testing"
"github.com/mitchellh/cli"
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
)
func testAuthHelpCommand(tb testing.TB) (*cli.MockUi, *AuthHelpCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &AuthHelpCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
Handlers: map[string]LoginHandler{
"userpass": &credUserpass.CLIHandler{
DefaultMount: "userpass",
},
},
}
}
func TestAuthHelpCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"too_many_args",
[]string{"foo", "bar"},
"Too many arguments",
1,
},
{
"not_enough_args",
nil,
"Not enough arguments",
1,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ui, cmd := testAuthHelpCommand(t)
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
t.Run("path", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
if err := client.Sys().EnableAuth("foo", "userpass", ""); err != nil {
t.Fatal(err)
}
ui, cmd := testAuthHelpCommand(t)
cmd.client = client
code := cmd.Run([]string{
"foo/",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Usage: vault login -method=userpass"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("type", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
// No mounted auth methods
ui, cmd := testAuthHelpCommand(t)
cmd.client = client
code := cmd.Run([]string{
"userpass",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Usage: vault login -method=userpass"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testAuthHelpCommand(t)
cmd.client = client
code := cmd.Run([]string{
"sys/mounts",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error listing auth methods: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testAuthHelpCommand(t)
assertNoTabs(t, cmd)
})
}

167
command/auth_list.go Normal file
View file

@ -0,0 +1,167 @@
package command
import (
"fmt"
"sort"
"strconv"
"strings"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*AuthListCommand)(nil)
var _ cli.CommandAutocomplete = (*AuthListCommand)(nil)
type AuthListCommand struct {
*BaseCommand
flagDetailed bool
}
func (c *AuthListCommand) Synopsis() string {
return "Lists enabled auth methods"
}
func (c *AuthListCommand) Help() string {
helpText := `
Usage: vault auth list [options]
Lists the enabled auth methods on the Vault server. This command also outputs
information about the method including configuration and human-friendly
descriptions. A TTL of "system" indicates that the system default is in use.
List all enabled auth methods:
$ vault auth list
List all enabled auth methods with detailed output:
$ vault auth list -detailed
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *AuthListCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP)
f := set.NewFlagSet("Command Options")
f.BoolVar(&BoolVar{
Name: "detailed",
Target: &c.flagDetailed,
Default: false,
Usage: "Print detailed information such as configuration and replication " +
"status about each auth method.",
})
return set
}
func (c *AuthListCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *AuthListCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *AuthListCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
if len(args) > 0 {
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
auths, err := client.Sys().ListAuth()
if err != nil {
c.UI.Error(fmt.Sprintf("Error listing enabled authentications: %s", err))
return 2
}
if c.flagDetailed {
c.UI.Output(tableOutput(c.detailedMounts(auths), nil))
return 0
}
c.UI.Output(tableOutput(c.simpleMounts(auths), nil))
return 0
}
func (c *AuthListCommand) simpleMounts(auths map[string]*api.AuthMount) []string {
paths := make([]string, 0, len(auths))
for path := range auths {
paths = append(paths, path)
}
sort.Strings(paths)
out := []string{"Path | Type | Description"}
for _, path := range paths {
mount := auths[path]
out = append(out, fmt.Sprintf("%s | %s | %s", path, mount.Type, mount.Description))
}
return out
}
func (c *AuthListCommand) detailedMounts(auths map[string]*api.AuthMount) []string {
paths := make([]string, 0, len(auths))
for path := range auths {
paths = append(paths, path)
}
sort.Strings(paths)
calcTTL := func(typ string, ttl int) string {
switch {
case typ == "system", typ == "cubbyhole":
return ""
case ttl != 0:
return strconv.Itoa(ttl)
default:
return "system"
}
}
out := []string{"Path | Type | Accessor | Plugin | Default TTL | Max TTL | Replication | Seal Wrap | Description"}
for _, path := range paths {
mount := auths[path]
defaultTTL := calcTTL(mount.Type, mount.Config.DefaultLeaseTTL)
maxTTL := calcTTL(mount.Type, mount.Config.MaxLeaseTTL)
replication := "replicated"
if mount.Local {
replication = "local"
}
out = append(out, fmt.Sprintf("%s | %s | %s | %s | %s | %s | %s | %t | %s",
path,
mount.Type,
mount.Accessor,
mount.Config.PluginName,
defaultTTL,
maxTTL,
replication,
mount.SealWrap,
mount.Description,
))
}
return out
}

105
command/auth_list_test.go Normal file
View file

@ -0,0 +1,105 @@
package command
import (
"strings"
"testing"
"github.com/mitchellh/cli"
)
func testAuthListCommand(tb testing.TB) (*cli.MockUi, *AuthListCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &AuthListCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestAuthListCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"too_many_args",
[]string{"foo"},
"Too many arguments",
1,
},
{
"lists",
nil,
"Path",
0,
},
{
"detailed",
[]string{"-detailed"},
"Default TTL",
0,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testAuthListCommand(t)
cmd.client = client
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testAuthListCommand(t)
cmd.client = client
code := cmd.Run([]string{})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error listing enabled authentications: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testAuthListCommand(t)
assertNoTabs(t, cmd)
})
}

View file

@ -1,400 +1,135 @@
package command
import (
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"testing"
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
credToken "github.com/hashicorp/vault/builtin/credential/token"
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
"github.com/hashicorp/vault/command/token"
)
func TestAuth_methods(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
func testAuthCommand(tb testing.TB) (*cli.MockUi, *AuthCommand) {
tb.Helper()
testAuthInit(t)
ui := cli.NewMockUi()
return ui, &AuthCommand{
BaseCommand: &BaseCommand{
UI: ui,
ui := new(cli.MockUi)
c := &AuthCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
TokenHelper: DefaultTokenHelper,
// Override to our own token helper
tokenHelper: token.NewTestingTokenHelper(),
},
Handlers: map[string]LoginHandler{
"token": &credToken.CLIHandler{},
"userpass": &credUserpass.CLIHandler{},
},
}
args := []string{
"-address", addr,
"-methods",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
if !strings.Contains(output, "token") {
t.Fatalf("bad: %#v", output)
}
}
func TestAuth_token(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
func TestAuthCommand_Run(t *testing.T) {
t.Parallel()
testAuthInit(t)
// TODO: remove in 0.9.0
t.Run("deprecated_methods", func(t *testing.T) {
t.Parallel()
ui := new(cli.MockUi)
c := &AuthCommand{
Meta: meta.Meta{
Ui: ui,
TokenHelper: DefaultTokenHelper,
},
}
client, closer := testVaultServer(t)
defer closer()
args := []string{
"-address", addr,
token,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
ui, cmd := testAuthCommand(t)
cmd.client = client
helper, err := c.TokenHelper()
if err != nil {
t.Fatalf("err: %s", err)
}
// vault auth -methods -> vault auth list
code := cmd.Run([]string{"-methods"})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
stdout, stderr := ui.OutputWriter.String(), ui.ErrorWriter.String()
actual, err := helper.Get()
if err != nil {
t.Fatalf("err: %s", err)
}
if expected := "WARNING!"; !strings.Contains(stderr, expected) {
t.Errorf("expected %q to contain %q", stderr, expected)
}
if actual != token {
t.Fatalf("bad: %s", actual)
}
}
func TestAuth_wrapping(t *testing.T) {
baseConfig := &vault.CoreConfig{
CredentialBackends: map[string]logical.Factory{
"userpass": credUserpass.Factory,
},
}
cluster := vault.NewTestCluster(t, baseConfig, &vault.TestClusterOptions{
HandlerFunc: http.Handler,
BaseListenAddress: "127.0.0.1:8200",
if expected := "token/"; !strings.Contains(stdout, expected) {
t.Errorf("expected %q to contain %q", stdout, expected)
}
})
cluster.Start()
defer cluster.Cleanup()
testAuthInit(t)
t.Run("deprecated_method_help", func(t *testing.T) {
t.Parallel()
client := cluster.Cores[0].Client
err := client.Sys().EnableAuthWithOptions("userpass", &api.EnableAuthOptions{
Type: "userpass",
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testAuthCommand(t)
cmd.client = client
// vault auth -method=foo -method-help -> vault auth help foo
code := cmd.Run([]string{
"-method=userpass",
"-method-help",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
stdout, stderr := ui.OutputWriter.String(), ui.ErrorWriter.String()
if expected := "WARNING!"; !strings.Contains(stderr, expected) {
t.Errorf("expected %q to contain %q", stderr, expected)
}
if expected := "vault login"; !strings.Contains(stdout, expected) {
t.Errorf("expected %q to contain %q", stdout, expected)
}
})
if err != nil {
t.Fatal(err)
}
_, err = client.Logical().Write("auth/userpass/users/foo", map[string]interface{}{
"password": "bar",
"policies": "zip,zap",
t.Run("deprecated_login", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
if err := client.Sys().EnableAuth("my-auth", "userpass", ""); err != nil {
t.Fatal(err)
}
if _, err := client.Logical().Write("auth/my-auth/users/test", map[string]interface{}{
"password": "test",
"policies": "default",
}); err != nil {
t.Fatal(err)
}
ui, cmd := testAuthCommand(t)
cmd.client = client
// vault auth ARGS -> vault login ARGS
code := cmd.Run([]string{
"-method", "userpass",
"-path", "my-auth",
"username=test",
"password=test",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
stdout, stderr := ui.OutputWriter.String(), ui.ErrorWriter.String()
if expected := "WARNING!"; !strings.Contains(stderr, expected) {
t.Errorf("expected %q to contain %q", stderr, expected)
}
if expected := "Success! You are now authenticated."; !strings.Contains(stdout, expected) {
t.Errorf("expected %q to contain %q", stdout, expected)
}
})
if err != nil {
t.Fatal(err)
}
ui := new(cli.MockUi)
c := &AuthCommand{
Meta: meta.Meta{
Ui: ui,
TokenHelper: DefaultTokenHelper,
},
Handlers: map[string]AuthHandler{
"userpass": &credUserpass.CLIHandler{DefaultMount: "userpass"},
},
}
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
args := []string{
"-address",
"https://127.0.0.1:8200",
"-tls-skip-verify",
"-method",
"userpass",
"username=foo",
"password=bar",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Test again with wrapping
ui = new(cli.MockUi)
c = &AuthCommand{
Meta: meta.Meta{
Ui: ui,
TokenHelper: DefaultTokenHelper,
},
Handlers: map[string]AuthHandler{
"userpass": &credUserpass.CLIHandler{DefaultMount: "userpass"},
},
}
args = []string{
"-address",
"https://127.0.0.1:8200",
"-tls-skip-verify",
"-wrap-ttl",
"5m",
"-method",
"userpass",
"username=foo",
"password=bar",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Test again with no-store
ui = new(cli.MockUi)
c = &AuthCommand{
Meta: meta.Meta{
Ui: ui,
TokenHelper: DefaultTokenHelper,
},
Handlers: map[string]AuthHandler{
"userpass": &credUserpass.CLIHandler{DefaultMount: "userpass"},
},
}
args = []string{
"-address",
"https://127.0.0.1:8200",
"-tls-skip-verify",
"-wrap-ttl",
"5m",
"-no-store",
"-method",
"userpass",
"username=foo",
"password=bar",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Test again with wrapping and token-only
ui = new(cli.MockUi)
c = &AuthCommand{
Meta: meta.Meta{
Ui: ui,
TokenHelper: DefaultTokenHelper,
},
Handlers: map[string]AuthHandler{
"userpass": &credUserpass.CLIHandler{DefaultMount: "userpass"},
},
}
args = []string{
"-address",
"https://127.0.0.1:8200",
"-tls-skip-verify",
"-wrap-ttl",
"5m",
"-token-only",
"-method",
"userpass",
"username=foo",
"password=bar",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
token := strings.TrimSpace(ui.OutputWriter.String())
if token == "" {
t.Fatal("expected to find token in output")
}
secret, err := client.Logical().Unwrap(token)
if err != nil {
t.Fatal(err)
}
if secret.Auth.ClientToken == "" {
t.Fatal("no client token found")
}
_, cmd := testAuthCommand(t)
assertNoTabs(t, cmd)
})
}
func TestAuth_token_nostore(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
testAuthInit(t)
ui := new(cli.MockUi)
c := &AuthCommand{
Meta: meta.Meta{
Ui: ui,
TokenHelper: DefaultTokenHelper,
},
}
args := []string{
"-address", addr,
"-no-store",
token,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
helper, err := c.TokenHelper()
if err != nil {
t.Fatalf("err: %s", err)
}
actual, err := helper.Get()
if err != nil {
t.Fatalf("err: %s", err)
}
if actual != "" {
t.Fatalf("bad: %s", actual)
}
}
func TestAuth_stdin(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
testAuthInit(t)
stdinR, stdinW := io.Pipe()
ui := new(cli.MockUi)
c := &AuthCommand{
Meta: meta.Meta{
Ui: ui,
TokenHelper: DefaultTokenHelper,
},
testStdin: stdinR,
}
go func() {
stdinW.Write([]byte(token))
stdinW.Close()
}()
args := []string{
"-address", addr,
"-",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
func TestAuth_badToken(t *testing.T) {
core, _, _ := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
testAuthInit(t)
ui := new(cli.MockUi)
c := &AuthCommand{
Meta: meta.Meta{
Ui: ui,
TokenHelper: DefaultTokenHelper,
},
}
args := []string{
"-address", addr,
"not-a-valid-token",
}
if code := c.Run(args); code != 1 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
func TestAuth_method(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
testAuthInit(t)
ui := new(cli.MockUi)
c := &AuthCommand{
Handlers: map[string]AuthHandler{
"test": &testAuthHandler{},
},
Meta: meta.Meta{
Ui: ui,
TokenHelper: DefaultTokenHelper,
},
}
args := []string{
"-address", addr,
"-method=test",
"foo=" + token,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
helper, err := c.TokenHelper()
if err != nil {
t.Fatalf("err: %s", err)
}
actual, err := helper.Get()
if err != nil {
t.Fatalf("err: %s", err)
}
if actual != token {
t.Fatalf("bad: %s", actual)
}
}
func testAuthInit(t *testing.T) {
td, err := ioutil.TempDir("", "vault")
if err != nil {
t.Fatalf("err: %s", err)
}
// Set the HOME env var so we get that right
os.Setenv("HOME", td)
// Write a .vault config to use our custom token helper
config := fmt.Sprintf(
"token_helper = \"\"\n")
ioutil.WriteFile(filepath.Join(td, ".vault"), []byte(config), 0644)
}
type testAuthHandler struct{}
func (h *testAuthHandler) Auth(c *api.Client, m map[string]string) (*api.Secret, error) {
return &api.Secret{
Auth: &api.SecretAuth{
ClientToken: m["foo"],
},
}, nil
}
func (h *testAuthHandler) Help() string { return "" }

120
command/auth_tune.go Normal file
View file

@ -0,0 +1,120 @@
package command
import (
"fmt"
"strings"
"time"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*AuthTuneCommand)(nil)
var _ cli.CommandAutocomplete = (*AuthTuneCommand)(nil)
type AuthTuneCommand struct {
*BaseCommand
flagDefaultLeaseTTL time.Duration
flagMaxLeaseTTL time.Duration
}
func (c *AuthTuneCommand) Synopsis() string {
return "Tunes an auth method configuration"
}
func (c *AuthTuneCommand) Help() string {
helpText := `
Usage: vault auth tune [options] PATH
Tunes the configuration options for the auth method at the given PATH. The
argument corresponds to the PATH where the auth method is enabled, not the
TYPE!
Tune the default lease for the github auth method:
$ vault auth tune -default-lease-ttl=72h github/
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *AuthTuneCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP)
f := set.NewFlagSet("Command Options")
f.DurationVar(&DurationVar{
Name: "default-lease-ttl",
Target: &c.flagDefaultLeaseTTL,
Default: 0,
EnvVar: "",
Completion: complete.PredictAnything,
Usage: "The default lease TTL for this auth method. If unspecified, this " +
"defaults to the Vault server's globally configured default lease TTL, " +
"or a previously configured value for the auth method.",
})
f.DurationVar(&DurationVar{
Name: "max-lease-ttl",
Target: &c.flagMaxLeaseTTL,
Default: 0,
EnvVar: "",
Completion: complete.PredictAnything,
Usage: "The maximum lease TTL for this auth method. If unspecified, this " +
"defaults to the Vault server's globally configured maximum lease TTL, " +
"or a previously configured value for the auth method.",
})
return set
}
func (c *AuthTuneCommand) AutocompleteArgs() complete.Predictor {
return c.PredictVaultAuths()
}
func (c *AuthTuneCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *AuthTuneCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
// Append /auth (since that's where auths live) and a trailing slash to
// indicate it's a path in output
mountPath := ensureTrailingSlash(sanitizePath(args[0]))
if err := client.Sys().TuneMount("/auth/"+mountPath, api.MountConfigInput{
DefaultLeaseTTL: ttlToAPI(c.flagDefaultLeaseTTL),
MaxLeaseTTL: ttlToAPI(c.flagMaxLeaseTTL),
}); err != nil {
c.UI.Error(fmt.Sprintf("Error tuning auth method %s: %s", mountPath, err))
return 2
}
c.UI.Output(fmt.Sprintf("Success! Tuned the auth method at: %s", mountPath))
return 0
}

149
command/auth_tune_test.go Normal file
View file

@ -0,0 +1,149 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
)
func testAuthTuneCommand(tb testing.TB) (*cli.MockUi, *AuthTuneCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &AuthTuneCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestAuthTuneCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"not_enough_args",
[]string{},
"Not enough arguments",
1,
},
{
"too_many_args",
[]string{"foo", "bar"},
"Too many arguments",
1,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ui, cmd := testAuthTuneCommand(t)
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("integration", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testAuthTuneCommand(t)
cmd.client = client
// Mount
if err := client.Sys().EnableAuthWithOptions("my-auth", &api.EnableAuthOptions{
Type: "userpass",
}); err != nil {
t.Fatal(err)
}
code := cmd.Run([]string{
"-default-lease-ttl", "30m",
"-max-lease-ttl", "1h",
"my-auth/",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Success! Tuned the auth method at: my-auth/"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
auths, err := client.Sys().ListAuth()
if err != nil {
t.Fatal(err)
}
mountInfo, ok := auths["my-auth/"]
if !ok {
t.Fatalf("expected auth to exist")
}
if exp := "userpass"; mountInfo.Type != exp {
t.Errorf("expected %q to be %q", mountInfo.Type, exp)
}
if exp := 1800; mountInfo.Config.DefaultLeaseTTL != exp {
t.Errorf("expected %d to be %d", mountInfo.Config.DefaultLeaseTTL, exp)
}
if exp := 3600; mountInfo.Config.MaxLeaseTTL != exp {
t.Errorf("expected %d to be %d", mountInfo.Config.MaxLeaseTTL, exp)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testAuthTuneCommand(t)
cmd.client = client
code := cmd.Run([]string{
"userpass/",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error tuning auth method userpass/: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testAuthTuneCommand(t)
assertNoTabs(t, cmd)
})
}

399
command/base.go Normal file
View file

@ -0,0 +1,399 @@
package command
import (
"bytes"
"flag"
"fmt"
"io"
"io/ioutil"
"regexp"
"strings"
"sync"
"time"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/command/token"
"github.com/mitchellh/cli"
"github.com/pkg/errors"
"github.com/posener/complete"
)
// maxLineLength is the maximum width of any line.
const maxLineLength int = 78
// reRemoveWhitespace is a regular expression for stripping whitespace from
// a string.
var reRemoveWhitespace = regexp.MustCompile(`[\s]+`)
type BaseCommand struct {
UI cli.Ui
flags *FlagSets
flagsOnce sync.Once
flagAddress string
flagCACert string
flagCAPath string
flagClientCert string
flagClientKey string
flagTLSServerName string
flagTLSSkipVerify bool
flagWrapTTL time.Duration
flagFormat string
flagField string
tokenHelper token.TokenHelper
// For testing
client *api.Client
}
// Client returns the HTTP API client. The client is cached on the command to
// save performance on future calls.
func (c *BaseCommand) Client() (*api.Client, error) {
// Read the test client if present
if c.client != nil {
return c.client, nil
}
config := api.DefaultConfig()
if err := config.ReadEnvironment(); err != nil {
return nil, errors.Wrap(err, "failed to read environment")
}
if c.flagAddress != "" {
config.Address = c.flagAddress
}
// If we need custom TLS configuration, then set it
if c.flagCACert != "" || c.flagCAPath != "" || c.flagClientCert != "" ||
c.flagClientKey != "" || c.flagTLSServerName != "" || c.flagTLSSkipVerify {
t := &api.TLSConfig{
CACert: c.flagCACert,
CAPath: c.flagCAPath,
ClientCert: c.flagClientCert,
ClientKey: c.flagClientKey,
TLSServerName: c.flagTLSServerName,
Insecure: c.flagTLSSkipVerify,
}
config.ConfigureTLS(t)
}
// Build the client
client, err := api.NewClient(config)
if err != nil {
return nil, errors.Wrap(err, "failed to create client")
}
// Set the wrapping function
client.SetWrappingLookupFunc(c.DefaultWrappingLookupFunc)
// Get the token if it came in from the environment
token := client.Token()
// If we don't have a token, check the token helper
if token == "" {
helper, err := c.TokenHelper()
if err != nil {
return nil, errors.Wrap(err, "failed to get token helper")
}
token, err = helper.Get()
if err != nil {
return nil, errors.Wrap(err, "failed to get token from token helper")
}
}
// Set the token
if token != "" {
client.SetToken(token)
}
return client, nil
}
// TokenHelper returns the token helper attached to the command.
func (c *BaseCommand) TokenHelper() (token.TokenHelper, error) {
if c.tokenHelper != nil {
return c.tokenHelper, nil
}
helper, err := DefaultTokenHelper()
if err != nil {
return nil, err
}
return helper, nil
}
// DefaultWrappingLookupFunc is the default wrapping function based on the
// CLI flag.
func (c *BaseCommand) DefaultWrappingLookupFunc(operation, path string) string {
if c.flagWrapTTL != 0 {
return c.flagWrapTTL.String()
}
return api.DefaultWrappingLookupFunc(operation, path)
}
type FlagSetBit uint
const (
FlagSetNone FlagSetBit = 1 << iota
FlagSetHTTP
FlagSetOutputField
FlagSetOutputFormat
)
// flagSet creates the flags for this command. The result is cached on the
// command to save performance on future calls.
func (c *BaseCommand) flagSet(bit FlagSetBit) *FlagSets {
c.flagsOnce.Do(func() {
set := NewFlagSets(c.UI)
if bit&FlagSetHTTP != 0 {
f := set.NewFlagSet("HTTP Options")
f.StringVar(&StringVar{
Name: "address",
Target: &c.flagAddress,
Default: "https://127.0.0.1:8200",
EnvVar: "VAULT_ADDR",
Completion: complete.PredictAnything,
Usage: "Address of the Vault server.",
})
f.StringVar(&StringVar{
Name: "ca-cert",
Target: &c.flagCACert,
Default: "",
EnvVar: "VAULT_CACERT",
Completion: complete.PredictFiles("*"),
Usage: "Path on the local disk to a single PEM-encoded CA " +
"certificate to verify the Vault server's SSL certificate. This " +
"takes precendence over -ca-path.",
})
f.StringVar(&StringVar{
Name: "ca-path",
Target: &c.flagCAPath,
Default: "",
EnvVar: "VAULT_CAPATH",
Completion: complete.PredictDirs("*"),
Usage: "Path on the local disk to a directory of PEM-encoded CA " +
"certificates to verify the Vault server's SSL certificate.",
})
f.StringVar(&StringVar{
Name: "client-cert",
Target: &c.flagClientCert,
Default: "",
EnvVar: "VAULT_CLIENT_CERT",
Completion: complete.PredictFiles("*"),
Usage: "Path on the local disk to a single PEM-encoded CA " +
"certificate to use for TLS authentication to the Vault server. If " +
"this flag is specified, -client-key is also required.",
})
f.StringVar(&StringVar{
Name: "client-key",
Target: &c.flagClientKey,
Default: "",
EnvVar: "VAULT_CLIENT_KEY",
Completion: complete.PredictFiles("*"),
Usage: "Path on the local disk to a single PEM-encoded private key " +
"matching the client certificate from -client-cert.",
})
f.StringVar(&StringVar{
Name: "tls-server-name",
Target: &c.flagTLSServerName,
Default: "",
EnvVar: "VAULT_TLS_SERVER_NAME",
Completion: complete.PredictAnything,
Usage: "Name to use as the SNI host when connecting to the Vault " +
"server via TLS.",
})
f.BoolVar(&BoolVar{
Name: "tls-skip-verify",
Target: &c.flagTLSSkipVerify,
Default: false,
EnvVar: "VAULT_SKIP_VERIFY",
Usage: "Disable verification of TLS certificates. Using this option " +
"is highly discouraged and decreases the security of data " +
"transmissions to and from the Vault server.",
})
f.DurationVar(&DurationVar{
Name: "wrap-ttl",
Target: &c.flagWrapTTL,
Default: 0,
EnvVar: "VAULT_WRAP_TTL",
Completion: complete.PredictAnything,
Usage: "Wraps the response in a cubbyhole token with the requested " +
"TTL. The response is available via the \"vault unwrap\" command. " +
"The TTL is specified as a numeric string with suffix like \"30s\" " +
"or \"5m\".",
})
}
if bit&(FlagSetOutputField|FlagSetOutputFormat) != 0 {
f := set.NewFlagSet("Output Options")
if bit&FlagSetOutputField != 0 {
f.StringVar(&StringVar{
Name: "field",
Target: &c.flagField,
Default: "",
Completion: complete.PredictAnything,
Usage: "Print only the field with the given name. Specifying " +
"this option will take precedence over other formatting " +
"directives. The result will not have a trailing newline " +
"making it idea for piping to other processes.",
})
}
if bit&FlagSetOutputFormat != 0 {
f.StringVar(&StringVar{
Name: "format",
Target: &c.flagFormat,
Default: "table",
EnvVar: "VAULT_FORMAT",
Completion: complete.PredictSet("table", "json", "yaml"),
Usage: "Print the output in the given format. Valid formats " +
"are \"table\", \"json\", or \"yaml\".",
})
}
}
c.flags = set
})
return c.flags
}
// FlagSets is a group of flag sets.
type FlagSets struct {
flagSets []*FlagSet
mainSet *flag.FlagSet
hiddens map[string]struct{}
completions complete.Flags
}
// NewFlagSets creates a new flag sets.
func NewFlagSets(ui cli.Ui) *FlagSets {
mainSet := flag.NewFlagSet("", flag.ContinueOnError)
// Errors and usage are controlled by the CLI.
mainSet.Usage = func() {}
mainSet.SetOutput(ioutil.Discard)
return &FlagSets{
flagSets: make([]*FlagSet, 0, 6),
mainSet: mainSet,
hiddens: make(map[string]struct{}),
completions: complete.Flags{},
}
}
// NewFlagSet creates a new flag set from the given flag sets.
func (f *FlagSets) NewFlagSet(name string) *FlagSet {
flagSet := NewFlagSet(name)
flagSet.mainSet = f.mainSet
flagSet.completions = f.completions
f.flagSets = append(f.flagSets, flagSet)
return flagSet
}
// Completions returns the completions for this flag set.
func (f *FlagSets) Completions() complete.Flags {
return f.completions
}
// Parse parses the given flags, returning any errors.
func (f *FlagSets) Parse(args []string) error {
return f.mainSet.Parse(args)
}
// Args returns the remaining args after parsing.
func (f *FlagSets) Args() []string {
return f.mainSet.Args()
}
// Help builds custom help for this command, grouping by flag set.
func (fs *FlagSets) Help() string {
var out bytes.Buffer
for _, set := range fs.flagSets {
printFlagTitle(&out, set.name+":")
set.VisitAll(func(f *flag.Flag) {
// Skip any hidden flags
if v, ok := f.Value.(FlagVisibility); ok && v.Hidden() {
return
}
printFlagDetail(&out, f)
})
}
return strings.TrimRight(out.String(), "\n")
}
// FlagSet is a grouped wrapper around a real flag set and a grouped flag set.
type FlagSet struct {
name string
flagSet *flag.FlagSet
mainSet *flag.FlagSet
completions complete.Flags
}
// NewFlagSet creates a new flag set.
func NewFlagSet(name string) *FlagSet {
return &FlagSet{
name: name,
flagSet: flag.NewFlagSet(name, flag.ContinueOnError),
}
}
// Name returns the name of this flag set.
func (f *FlagSet) Name() string {
return f.name
}
func (f *FlagSet) Visit(fn func(*flag.Flag)) {
f.flagSet.Visit(fn)
}
func (f *FlagSet) VisitAll(fn func(*flag.Flag)) {
f.flagSet.VisitAll(fn)
}
// printFlagTitle prints a consistently-formatted title to the given writer.
func printFlagTitle(w io.Writer, s string) {
fmt.Fprintf(w, "%s\n\n", s)
}
// printFlagDetail prints a single flag to the given writer.
func printFlagDetail(w io.Writer, f *flag.Flag) {
// Check if the flag is hidden - do not print any flag detail or help output
// if it is hidden.
if h, ok := f.Value.(FlagVisibility); ok && h.Hidden() {
return
}
// Check for a detailed example
example := ""
if t, ok := f.Value.(FlagExample); ok {
example = t.Example()
}
if example != "" {
fmt.Fprintf(w, " -%s=<%s>\n", f.Name, example)
} else {
fmt.Fprintf(w, " -%s\n", f.Name)
}
usage := reRemoveWhitespace.ReplaceAllString(f.Usage, " ")
indented := wrapAtLengthWithPadding(usage, 6)
fmt.Fprintf(w, "%s\n\n", indented)
}

780
command/base_flags.go Normal file
View file

@ -0,0 +1,780 @@
package command
import (
"flag"
"fmt"
"os"
"sort"
"strconv"
"strings"
"time"
"github.com/posener/complete"
)
// FlagExample is an interface which declares an example value.
type FlagExample interface {
Example() string
}
// FlagVisibility is an interface which declares whether a flag should be
// hidden from help and completions. This is usually used for deprecations
// on "internal-only" flags.
type FlagVisibility interface {
Hidden() bool
}
// FlagBool is an interface which boolean flags implement.
type FlagBool interface {
IsBoolFlag() bool
}
// -- BoolVar and boolValue
type BoolVar struct {
Name string
Aliases []string
Usage string
Default bool
Hidden bool
EnvVar string
Target *bool
Completion complete.Predictor
}
func (f *FlagSet) BoolVar(i *BoolVar) {
def := i.Default
if v := os.Getenv(i.EnvVar); v != "" {
if b, err := strconv.ParseBool(v); err != nil {
def = b
}
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: strconv.FormatBool(i.Default),
EnvVar: i.EnvVar,
Value: newBoolValue(def, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type boolValue struct {
hidden bool
target *bool
}
func newBoolValue(def bool, target *bool, hidden bool) *boolValue {
*target = def
return &boolValue{
hidden: hidden,
target: target,
}
}
func (b *boolValue) Set(s string) error {
v, err := strconv.ParseBool(s)
if err != nil {
return err
}
*b.target = v
return nil
}
func (b *boolValue) Get() interface{} { return *b.target }
func (b *boolValue) String() string { return strconv.FormatBool(*b.target) }
func (b *boolValue) Example() string { return "" }
func (b *boolValue) Hidden() bool { return b.hidden }
func (b *boolValue) IsBoolFlag() bool { return true }
// -- IntVar and intValue
type IntVar struct {
Name string
Aliases []string
Usage string
Default int
Hidden bool
EnvVar string
Target *int
Completion complete.Predictor
}
func (f *FlagSet) IntVar(i *IntVar) {
initial := i.Default
if v := os.Getenv(i.EnvVar); v != "" {
if i, err := strconv.ParseInt(v, 0, 64); err != nil {
initial = int(i)
}
}
def := ""
if i.Default != 0 {
def = strconv.FormatInt(int64(i.Default), 10)
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
EnvVar: i.EnvVar,
Value: newIntValue(initial, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type intValue struct {
hidden bool
target *int
}
func newIntValue(def int, target *int, hidden bool) *intValue {
*target = def
return &intValue{
hidden: hidden,
target: target,
}
}
func (i *intValue) Set(s string) error {
v, err := strconv.ParseInt(s, 0, 64)
if err != nil {
return err
}
*i.target = int(v)
return nil
}
func (i *intValue) Get() interface{} { return int(*i.target) }
func (i *intValue) String() string { return strconv.Itoa(int(*i.target)) }
func (i *intValue) Example() string { return "int" }
func (i *intValue) Hidden() bool { return i.hidden }
// -- Int64Var and int64Value
type Int64Var struct {
Name string
Aliases []string
Usage string
Default int64
Hidden bool
EnvVar string
Target *int64
Completion complete.Predictor
}
func (f *FlagSet) Int64Var(i *Int64Var) {
initial := i.Default
if v := os.Getenv(i.EnvVar); v != "" {
if i, err := strconv.ParseInt(v, 0, 64); err != nil {
initial = i
}
}
def := ""
if i.Default != 0 {
def = strconv.FormatInt(int64(i.Default), 10)
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
EnvVar: i.EnvVar,
Value: newInt64Value(initial, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type int64Value struct {
hidden bool
target *int64
}
func newInt64Value(def int64, target *int64, hidden bool) *int64Value {
*target = def
return &int64Value{
hidden: hidden,
target: target,
}
}
func (i *int64Value) Set(s string) error {
v, err := strconv.ParseInt(s, 0, 64)
if err != nil {
return err
}
*i.target = v
return nil
}
func (i *int64Value) Get() interface{} { return int64(*i.target) }
func (i *int64Value) String() string { return strconv.FormatInt(int64(*i.target), 10) }
func (i *int64Value) Example() string { return "int" }
func (i *int64Value) Hidden() bool { return i.hidden }
// -- UintVar && uintValue
type UintVar struct {
Name string
Aliases []string
Usage string
Default uint
Hidden bool
EnvVar string
Target *uint
Completion complete.Predictor
}
func (f *FlagSet) UintVar(i *UintVar) {
initial := i.Default
if v := os.Getenv(i.EnvVar); v != "" {
if i, err := strconv.ParseUint(v, 0, 64); err != nil {
initial = uint(i)
}
}
def := ""
if i.Default != 0 {
def = strconv.FormatUint(uint64(i.Default), 10)
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
EnvVar: i.EnvVar,
Value: newUintValue(initial, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type uintValue struct {
hidden bool
target *uint
}
func newUintValue(def uint, target *uint, hidden bool) *uintValue {
*target = def
return &uintValue{
hidden: hidden,
target: target,
}
}
func (i *uintValue) Set(s string) error {
v, err := strconv.ParseUint(s, 0, 64)
if err != nil {
return err
}
*i.target = uint(v)
return nil
}
func (i *uintValue) Get() interface{} { return uint(*i.target) }
func (i *uintValue) String() string { return strconv.FormatUint(uint64(*i.target), 10) }
func (i *uintValue) Example() string { return "uint" }
func (i *uintValue) Hidden() bool { return i.hidden }
// -- Uint64Var and uint64Value
type Uint64Var struct {
Name string
Aliases []string
Usage string
Default uint64
Hidden bool
EnvVar string
Target *uint64
Completion complete.Predictor
}
func (f *FlagSet) Uint64Var(i *Uint64Var) {
initial := i.Default
if v := os.Getenv(i.EnvVar); v != "" {
if i, err := strconv.ParseUint(v, 0, 64); err != nil {
initial = i
}
}
def := ""
if i.Default != 0 {
strconv.FormatUint(i.Default, 10)
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
EnvVar: i.EnvVar,
Value: newUint64Value(initial, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type uint64Value struct {
hidden bool
target *uint64
}
func newUint64Value(def uint64, target *uint64, hidden bool) *uint64Value {
*target = def
return &uint64Value{
hidden: hidden,
target: target,
}
}
func (i *uint64Value) Set(s string) error {
v, err := strconv.ParseUint(s, 0, 64)
if err != nil {
return err
}
*i.target = v
return nil
}
func (i *uint64Value) Get() interface{} { return uint64(*i.target) }
func (i *uint64Value) String() string { return strconv.FormatUint(uint64(*i.target), 10) }
func (i *uint64Value) Example() string { return "uint" }
func (i *uint64Value) Hidden() bool { return i.hidden }
// -- StringVar and stringValue
type StringVar struct {
Name string
Aliases []string
Usage string
Default string
Hidden bool
EnvVar string
Target *string
Completion complete.Predictor
}
func (f *FlagSet) StringVar(i *StringVar) {
initial := i.Default
if v := os.Getenv(i.EnvVar); v != "" {
initial = v
}
def := ""
if i.Default != "" {
def = i.Default
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
EnvVar: i.EnvVar,
Value: newStringValue(initial, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type stringValue struct {
hidden bool
target *string
}
func newStringValue(def string, target *string, hidden bool) *stringValue {
*target = def
return &stringValue{
hidden: hidden,
target: target,
}
}
func (s *stringValue) Set(val string) error {
*s.target = val
return nil
}
func (s *stringValue) Get() interface{} { return *s.target }
func (s *stringValue) String() string { return *s.target }
func (s *stringValue) Example() string { return "string" }
func (s *stringValue) Hidden() bool { return s.hidden }
// -- Float64Var and float64Value
type Float64Var struct {
Name string
Aliases []string
Usage string
Default float64
Hidden bool
EnvVar string
Target *float64
Completion complete.Predictor
}
func (f *FlagSet) Float64Var(i *Float64Var) {
initial := i.Default
if v := os.Getenv(i.EnvVar); v != "" {
if i, err := strconv.ParseFloat(v, 64); err != nil {
initial = i
}
}
def := ""
if i.Default != 0 {
def = strconv.FormatFloat(i.Default, 'e', -1, 64)
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
EnvVar: i.EnvVar,
Value: newFloat64Value(initial, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type float64Value struct {
hidden bool
target *float64
}
func newFloat64Value(def float64, target *float64, hidden bool) *float64Value {
*target = def
return &float64Value{
hidden: hidden,
target: target,
}
}
func (f *float64Value) Set(s string) error {
v, err := strconv.ParseFloat(s, 64)
if err != nil {
return err
}
*f.target = v
return nil
}
func (f *float64Value) Get() interface{} { return float64(*f.target) }
func (f *float64Value) String() string { return strconv.FormatFloat(float64(*f.target), 'g', -1, 64) }
func (f *float64Value) Example() string { return "float" }
func (f *float64Value) Hidden() bool { return f.hidden }
// -- DurationVar and durationValue
type DurationVar struct {
Name string
Aliases []string
Usage string
Default time.Duration
Hidden bool
EnvVar string
Target *time.Duration
Completion complete.Predictor
}
func (f *FlagSet) DurationVar(i *DurationVar) {
initial := i.Default
if v := os.Getenv(i.EnvVar); v != "" {
if d, err := time.ParseDuration(appendDurationSuffix(v)); err != nil {
initial = d
}
}
def := ""
if i.Default != 0 {
def = i.Default.String()
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
EnvVar: i.EnvVar,
Value: newDurationValue(initial, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type durationValue struct {
hidden bool
target *time.Duration
}
func newDurationValue(def time.Duration, target *time.Duration, hidden bool) *durationValue {
*target = def
return &durationValue{
hidden: hidden,
target: target,
}
}
func (d *durationValue) Set(s string) error {
// Maintain bc for people specifying "system" as the value.
if s == "system" {
s = "-1"
}
v, err := time.ParseDuration(appendDurationSuffix(s))
if err != nil {
return err
}
*d.target = v
return nil
}
func (d *durationValue) Get() interface{} { return time.Duration(*d.target) }
func (d *durationValue) String() string { return (*d.target).String() }
func (d *durationValue) Example() string { return "duration" }
func (d *durationValue) Hidden() bool { return d.hidden }
// appendDurationSuffix is used as a backwards-compat tool for assuming users
// meant "seconds" when they do not provide a suffixed duration value.
func appendDurationSuffix(s string) string {
if strings.HasSuffix(s, "s") || strings.HasSuffix(s, "m") || strings.HasSuffix(s, "h") {
return s
}
return s + "s"
}
// -- StringSliceVar and stringSliceValue
type StringSliceVar struct {
Name string
Aliases []string
Usage string
Default []string
Hidden bool
EnvVar string
Target *[]string
Completion complete.Predictor
}
func (f *FlagSet) StringSliceVar(i *StringSliceVar) {
initial := i.Default
if v := os.Getenv(i.EnvVar); v != "" {
parts := strings.Split(v, ",")
for i := range parts {
parts[i] = strings.TrimSpace(parts[i])
}
initial = parts
}
def := ""
if i.Default != nil {
def = strings.Join(i.Default, ",")
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
EnvVar: i.EnvVar,
Value: newStringSliceValue(initial, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type stringSliceValue struct {
hidden bool
target *[]string
}
func newStringSliceValue(def []string, target *[]string, hidden bool) *stringSliceValue {
*target = def
return &stringSliceValue{
hidden: hidden,
target: target,
}
}
func (s *stringSliceValue) Set(val string) error {
*s.target = append(*s.target, strings.TrimSpace(val))
return nil
}
func (s *stringSliceValue) Get() interface{} { return *s.target }
func (s *stringSliceValue) String() string { return strings.Join(*s.target, ",") }
func (s *stringSliceValue) Example() string { return "string" }
func (s *stringSliceValue) Hidden() bool { return s.hidden }
// -- StringMapVar and stringMapValue
type StringMapVar struct {
Name string
Aliases []string
Usage string
Default map[string]string
Hidden bool
Target *map[string]string
Completion complete.Predictor
}
func (f *FlagSet) StringMapVar(i *StringMapVar) {
def := ""
if i.Default != nil {
def = mapToKV(i.Default)
}
f.VarFlag(&VarFlag{
Name: i.Name,
Aliases: i.Aliases,
Usage: i.Usage,
Default: def,
Value: newStringMapValue(i.Default, i.Target, i.Hidden),
Completion: i.Completion,
})
}
type stringMapValue struct {
hidden bool
target *map[string]string
}
func newStringMapValue(def map[string]string, target *map[string]string, hidden bool) *stringMapValue {
*target = def
return &stringMapValue{
hidden: hidden,
target: target,
}
}
func (s *stringMapValue) Set(val string) error {
idx := strings.Index(val, "=")
if idx == -1 {
return fmt.Errorf("Missing = in KV pair: %s", val)
}
if *s.target == nil {
*s.target = make(map[string]string)
}
k, v := val[0:idx], val[idx+1:]
(*s.target)[k] = v
return nil
}
func (s *stringMapValue) Get() interface{} { return *s.target }
func (s *stringMapValue) String() string { return mapToKV(*s.target) }
func (s *stringMapValue) Example() string { return "key=value" }
func (s *stringMapValue) Hidden() bool { return s.hidden }
func mapToKV(m map[string]string) string {
list := make([]string, 0, len(m))
for k, _ := range m {
list = append(list, k)
}
sort.Strings(list)
for i, k := range list {
list[i] = k + "=" + m[k]
}
return strings.Join(list, ",")
}
// -- VarFlag
type VarFlag struct {
Name string
Aliases []string
Usage string
Default string
EnvVar string
Value flag.Value
Completion complete.Predictor
}
func (f *FlagSet) VarFlag(i *VarFlag) {
// If the flag is marked as hidden, just add it to the set and return to
// avoid unnecessary computations here. We do not want to add completions or
// generate help output for hidden flags.
if v, ok := i.Value.(FlagVisibility); ok && v.Hidden() {
f.Var(i.Value, i.Name, "")
return
}
// Calculate the full usage
usage := i.Usage
if len(i.Aliases) > 0 {
sentence := make([]string, len(i.Aliases))
for i, a := range i.Aliases {
sentence[i] = fmt.Sprintf(`"-%s"`, a)
}
aliases := ""
switch len(sentence) {
case 0:
// impossible...
case 1:
aliases = sentence[0]
case 2:
aliases = sentence[0] + " and " + sentence[1]
default:
sentence[len(sentence)-1] = "and " + sentence[len(sentence)-1]
aliases = strings.Join(sentence, ", ")
}
usage += fmt.Sprintf(" This is aliased as %s.", aliases)
}
if i.Default != "" {
usage += fmt.Sprintf(" The default is %s.", i.Default)
}
if i.EnvVar != "" {
usage += fmt.Sprintf(" This can also be specified via the %s "+
"environment variable.", i.EnvVar)
}
// Add aliases to the main set
for _, a := range i.Aliases {
f.mainSet.Var(i.Value, a, "")
}
f.Var(i.Value, i.Name, usage)
f.completions["-"+i.Name] = i.Completion
}
// Var is a lower-level API for adding something to the flags. It should be used
// wtih caution, since it bypasses all validation. Consider VarFlag instead.
func (f *FlagSet) Var(value flag.Value, name, usage string) {
f.mainSet.Var(value, name, usage)
f.flagSet.Var(value, name, usage)
}
// -- helpers
func envDefault(key, def string) string {
if v := os.Getenv(key); v != "" {
return v
}
return def
}
func envBoolDefault(key string, def bool) bool {
if v := os.Getenv(key); v != "" {
b, err := strconv.ParseBool(v)
if err != nil {
panic(err)
}
return b
}
return def
}
func envDurationDefault(key string, def time.Duration) time.Duration {
if v := os.Getenv(key); v != "" {
d, err := time.ParseDuration(v)
if err != nil {
panic(err)
}
return d
}
return def
}

243
command/base_helpers.go Normal file
View file

@ -0,0 +1,243 @@
package command
import (
"fmt"
"io"
"strings"
"time"
"github.com/hashicorp/vault/api"
kvbuilder "github.com/hashicorp/vault/helper/kv-builder"
"github.com/kr/text"
homedir "github.com/mitchellh/go-homedir"
"github.com/mitchellh/mapstructure"
"github.com/pkg/errors"
"github.com/ryanuber/columnize"
)
// extractListData reads the secret and returns a typed list of data and a
// boolean indicating whether the extraction was successful.
func extractListData(secret *api.Secret) ([]interface{}, bool) {
if secret == nil || secret.Data == nil {
return nil, false
}
k, ok := secret.Data["keys"]
if !ok || k == nil {
return nil, false
}
i, ok := k.([]interface{})
return i, ok
}
// sanitizePath removes any leading or trailing things from a "path".
func sanitizePath(s string) string {
return ensureNoTrailingSlash(ensureNoLeadingSlash(strings.TrimSpace(s)))
}
// ensureTrailingSlash ensures the given string has a trailing slash.
func ensureTrailingSlash(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
for len(s) > 0 && s[len(s)-1] != '/' {
s = s + "/"
}
return s
}
// ensureNoTrailingSlash ensures the given string has a trailing slash.
func ensureNoTrailingSlash(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
for len(s) > 0 && s[len(s)-1] == '/' {
s = s[:len(s)-1]
}
return s
}
// ensureNoLeadingSlash ensures the given string has a trailing slash.
func ensureNoLeadingSlash(s string) string {
s = strings.TrimSpace(s)
if s == "" {
return ""
}
for len(s) > 0 && s[0] == '/' {
s = s[1:]
}
return s
}
// columnOuput prints the list of items as a table with no headers.
func columnOutput(list []string, c *columnize.Config) string {
if len(list) == 0 {
return ""
}
if c == nil {
c = &columnize.Config{}
}
if c.Glue == "" {
c.Glue = " "
}
if c.Empty == "" {
c.Empty = "n/a"
}
return columnize.Format(list, c)
}
// tableOutput prints the list of items as columns, where the first row is
// the list of headers.
func tableOutput(list []string, c *columnize.Config) string {
if len(list) == 0 {
return ""
}
delim := "|"
if c != nil && c.Delim != "" {
delim = c.Delim
}
underline := ""
headers := strings.Split(list[0], delim)
for i, h := range headers {
h = strings.TrimSpace(h)
u := strings.Repeat("-", len(h))
underline = underline + u
if i != len(headers)-1 {
underline = underline + delim
}
}
list = append(list, "")
copy(list[2:], list[1:])
list[1] = underline
return columnOutput(list, c)
}
// parseArgsData parses the given args in the format key=value into a map of
// the provided arguments. The given reader can also supply key=value pairs.
func parseArgsData(stdin io.Reader, args []string) (map[string]interface{}, error) {
builder := &kvbuilder.Builder{Stdin: stdin}
if err := builder.Add(args...); err != nil {
return nil, err
}
return builder.Map(), nil
}
// parseArgsDataString parses the args data and returns the values as strings.
// If the values cannot be represented as strings, an error is returned.
func parseArgsDataString(stdin io.Reader, args []string) (map[string]string, error) {
raw, err := parseArgsData(stdin, args)
if err != nil {
return nil, err
}
var result map[string]string
if err := mapstructure.WeakDecode(raw, &result); err != nil {
return nil, errors.Wrap(err, "failed to convert values to strings")
}
return result, nil
}
// truncateToSeconds truncates the given duaration to the number of seconds. If
// the duration is less than 1s, it is returned as 0. The integer represents
// the whole number unit of seconds for the duration.
func truncateToSeconds(d time.Duration) int {
d = d.Truncate(1 * time.Second)
// Handle the case where someone requested a ridiculously short increment -
// incremenents must be larger than a second.
if d < 1*time.Second {
return 0
}
return int(d.Seconds())
}
// printKeyStatus prints the KeyStatus response from the API.
func printKeyStatus(ks *api.KeyStatus) string {
return columnOutput([]string{
fmt.Sprintf("Key Term | %d", ks.Term),
fmt.Sprintf("Install Time | %s", ks.InstallTime.UTC().Format(time.RFC822)),
}, nil)
}
// expandPath takes a filepath and returns the full expanded path, accounting
// for user-relative things like ~/.
func expandPath(s string) string {
if s == "" {
return ""
}
e, err := homedir.Expand(s)
if err != nil {
return s
}
return e
}
// wrapAtLengthWithPadding wraps the given text at the maxLineLength, taking
// into account any provided left padding.
func wrapAtLengthWithPadding(s string, pad int) string {
wrapped := text.Wrap(s, maxLineLength-pad)
lines := strings.Split(wrapped, "\n")
for i, line := range lines {
lines[i] = strings.Repeat(" ", pad) + line
}
return strings.Join(lines, "\n")
}
// wrapAtLength wraps the given text to maxLineLength.
func wrapAtLength(s string) string {
return wrapAtLengthWithPadding(s, 0)
}
// ttlToAPI converts a user-supplied ttl into an API-compatible string. If
// the TTL is 0, this returns the empty string. If the TTL is negative, this
// returns "system" to indicate to use the system values. Otherwise, the
// time.Duration ttl is used.
func ttlToAPI(d time.Duration) string {
if d == 0 {
return ""
}
if d < 0 {
return "system"
}
return d.String()
}
// humanDuration prints the time duration without those pesky zeros.
func humanDuration(d time.Duration) string {
if d == 0 {
return "0s"
}
s := d.String()
if strings.HasSuffix(s, "m0s") {
s = s[:len(s)-2]
}
if idx := strings.Index(s, "h0m"); idx > 0 {
s = s[:idx+1] + s[idx+3:]
}
return s
}
// humanDurationInt prints the given int as if it were a time.Duration number
// of seconds.
func humanDurationInt(i int) string {
return humanDuration(time.Duration(i) * time.Second)
}

View file

@ -0,0 +1,162 @@
package command
import (
"fmt"
"io"
"io/ioutil"
"os"
"testing"
"time"
)
func TestParseArgsData(t *testing.T) {
t.Parallel()
t.Run("stdin_full", func(t *testing.T) {
t.Parallel()
stdinR, stdinW := io.Pipe()
go func() {
stdinW.Write([]byte(`{"foo":"bar"}`))
stdinW.Close()
}()
m, err := parseArgsData(stdinR, []string{"-"})
if err != nil {
t.Fatal(err)
}
if v, ok := m["foo"]; !ok || v != "bar" {
t.Errorf("expected %q to be %q", v, "bar")
}
})
t.Run("stdin_value", func(t *testing.T) {
t.Parallel()
stdinR, stdinW := io.Pipe()
go func() {
stdinW.Write([]byte(`bar`))
stdinW.Close()
}()
m, err := parseArgsData(stdinR, []string{"foo=-"})
if err != nil {
t.Fatal(err)
}
if v, ok := m["foo"]; !ok || v != "bar" {
t.Errorf("expected %q to be %q", v, "bar")
}
})
t.Run("file_full", func(t *testing.T) {
t.Parallel()
f, err := ioutil.TempFile("", "vault")
if err != nil {
t.Fatal(err)
}
f.Write([]byte(`{"foo":"bar"}`))
f.Close()
defer os.Remove(f.Name())
m, err := parseArgsData(os.Stdin, []string{"@" + f.Name()})
if err != nil {
t.Fatal(err)
}
if v, ok := m["foo"]; !ok || v != "bar" {
t.Errorf("expected %q to be %q", v, "bar")
}
})
t.Run("file_value", func(t *testing.T) {
t.Parallel()
f, err := ioutil.TempFile("", "vault")
if err != nil {
t.Fatal(err)
}
f.Write([]byte(`bar`))
f.Close()
defer os.Remove(f.Name())
m, err := parseArgsData(os.Stdin, []string{"foo=@" + f.Name()})
if err != nil {
t.Fatal(err)
}
if v, ok := m["foo"]; !ok || v != "bar" {
t.Errorf("expected %q to be %q", v, "bar")
}
})
t.Run("file_value_escaped", func(t *testing.T) {
t.Parallel()
m, err := parseArgsData(os.Stdin, []string{`foo=\@`})
if err != nil {
t.Fatal(err)
}
if v, ok := m["foo"]; !ok || v != "@" {
t.Errorf("expected %q to be %q", v, "@")
}
})
}
func TestTruncateToSeconds(t *testing.T) {
t.Parallel()
cases := []struct {
d time.Duration
exp int
}{
{
10 * time.Nanosecond,
0,
},
{
10 * time.Microsecond,
0,
},
{
10 * time.Millisecond,
0,
},
{
1 * time.Second,
1,
},
{
10 * time.Second,
10,
},
{
100 * time.Second,
100,
},
{
3 * time.Minute,
180,
},
{
3 * time.Hour,
10800,
},
}
for _, tc := range cases {
tc := tc
t.Run(fmt.Sprintf("%s", tc.d), func(t *testing.T) {
t.Parallel()
act := truncateToSeconds(tc.d)
if act != tc.exp {
t.Errorf("expected %d to be %d", act, tc.exp)
}
})
}
}

417
command/base_predict.go Normal file
View file

@ -0,0 +1,417 @@
package command
import (
"sort"
"strings"
"sync"
"github.com/hashicorp/vault/api"
"github.com/posener/complete"
)
type Predict struct {
client *api.Client
clientOnce sync.Once
}
func NewPredict() *Predict {
return &Predict{}
}
func (p *Predict) Client() *api.Client {
p.clientOnce.Do(func() {
if p.client == nil { // For tests
client, _ := api.NewClient(nil)
if client.Token() == "" {
helper, err := DefaultTokenHelper()
if err != nil {
return
}
token, err := helper.Get()
if err != nil {
return
}
client.SetToken(token)
}
p.client = client
}
})
return p.client
}
// defaultPredictVaultMounts is the default list of mounts to return to the
// user. This is a best-guess, given we haven't communicated with the Vault
// server. If the user has no token or if the token does not have the default
// policy attached, it won't be able to read cubbyhole/, but it's a better UX
// that returning nothing.
var defaultPredictVaultMounts = []string{"cubbyhole/"}
// predictClient is the API client to use for prediction. We create this at the
// beginning once, because completions are generated for each command (and this
// doesn't change), and the only way to configure the predict/autocomplete
// client is via environment variables. Even if the user specifies a flag, we
// can't parse that flag until after the command is submitted.
var predictClient *api.Client
var predictClientOnce sync.Once
// PredictClient returns the cached API client for the predictor.
func PredictClient() *api.Client {
predictClientOnce.Do(func() {
if predictClient == nil { // For tests
predictClient, _ = api.NewClient(nil)
}
})
return predictClient
}
// PredictVaultAvailableMounts returns a predictor for the available mounts in
// Vault. For now, there is no way to programatically get this list. If, in the
// future, such a list exists, we can adapt it here. Until then, it's
// hard-coded.
func (b *BaseCommand) PredictVaultAvailableMounts() complete.Predictor {
// This list does not contain deprecated backends. At present, there is no
// API that lists all available secret backends, so this is hard-coded :(.
return complete.PredictSet(
"aws",
"consul",
"database",
"generic",
"pki",
"plugin",
"rabbitmq",
"ssh",
"totp",
"transit",
)
}
// PredictVaultAvailableAuths returns a predictor for the available auths in
// Vault. For now, there is no way to programatically get this list. If, in the
// future, such a list exists, we can adapt it here. Until then, it's
// hard-coded.
func (b *BaseCommand) PredictVaultAvailableAuths() complete.Predictor {
return complete.PredictSet(
"app-id",
"approle",
"aws",
"cert",
"gcp",
"github",
"ldap",
"okta",
"plugin",
"radius",
"userpass",
)
}
// PredictVaultFiles returns a predictor for Vault mounts and paths based on the
// configured client for the base command. Unfortunately this happens pre-flag
// parsing, so users must rely on environment variables for autocomplete if they
// are not using Vault at the default endpoints.
func (b *BaseCommand) PredictVaultFiles() complete.Predictor {
return NewPredict().VaultFiles()
}
// PredictVaultFolders returns a predictor for "folders". See PredictVaultFiles
// for more information and restrictions.
func (b *BaseCommand) PredictVaultFolders() complete.Predictor {
return NewPredict().VaultFolders()
}
// PredictVaultMounts returns a predictor for "folders". See PredictVaultFiles
// for more information and restrictions.
func (b *BaseCommand) PredictVaultMounts() complete.Predictor {
return NewPredict().VaultMounts()
}
// PredictVaultAudits returns a predictor for "folders". See PredictVaultFiles
// for more information and restrictions.
func (b *BaseCommand) PredictVaultAudits() complete.Predictor {
return NewPredict().VaultAudits()
}
// PredictVaultAuths returns a predictor for "folders". See PredictVaultFiles
// for more information and restrictions.
func (b *BaseCommand) PredictVaultAuths() complete.Predictor {
return NewPredict().VaultAuths()
}
// PredictVaultPolicies returns a predictor for "folders". See PredictVaultFiles
// for more information and restrictions.
func (b *BaseCommand) PredictVaultPolicies() complete.Predictor {
return NewPredict().VaultPolicies()
}
// VaultFiles returns a predictor for Vault "files". This is a public API for
// consumers, but you probably want BaseCommand.PredictVaultFiles instead.
func (p *Predict) VaultFiles() complete.Predictor {
return p.vaultPaths(true)
}
// VaultFolders returns a predictor for Vault "folders". This is a public
// API for consumers, but you probably want BaseCommand.PredictVaultFolders
// instead.
func (p *Predict) VaultFolders() complete.Predictor {
return p.vaultPaths(false)
}
// VaultMounts returns a predictor for Vault "folders". This is a public
// API for consumers, but you probably want BaseCommand.PredictVaultMounts
// instead.
func (p *Predict) VaultMounts() complete.Predictor {
return p.filterFunc(p.mounts)
}
// VaultAudits returns a predictor for Vault "folders". This is a public API for
// consumers, but you probably want BaseCommand.PredictVaultAudits instead.
func (p *Predict) VaultAudits() complete.Predictor {
return p.filterFunc(p.audits)
}
// VaultAuths returns a predictor for Vault "folders". This is a public API for
// consumers, but you probably want BaseCommand.PredictVaultAuths instead.
func (p *Predict) VaultAuths() complete.Predictor {
return p.filterFunc(p.auths)
}
// VaultPolicies returns a predictor for Vault "folders". This is a public API for
// consumers, but you probably want BaseCommand.PredictVaultPolicies instead.
func (p *Predict) VaultPolicies() complete.Predictor {
return p.filterFunc(p.policies)
}
// vaultPaths parses the CLI options and returns the "best" list of possible
// paths. If there are any errors, this function returns an empty result. All
// errors are suppressed since this is a prediction function.
func (p *Predict) vaultPaths(includeFiles bool) complete.PredictFunc {
return func(args complete.Args) []string {
// Do not predict more than one paths
if p.hasPathArg(args.All) {
return nil
}
client := p.Client()
if client == nil {
return nil
}
path := args.Last
var predictions []string
if strings.Contains(path, "/") {
predictions = p.paths(path, includeFiles)
} else {
predictions = p.filter(p.mounts(), path)
}
// Either no results or many results, so return.
if len(predictions) != 1 {
return predictions
}
// If this is not a "folder", do not try to recurse.
if !strings.HasSuffix(predictions[0], "/") {
return predictions
}
// If the prediction is the same as the last guess, return it (we have no
// new information and we won't get anymore).
if predictions[0] == args.Last {
return predictions
}
// Re-predict with the remaining path
args.Last = predictions[0]
return p.vaultPaths(includeFiles).Predict(args)
}
}
// paths predicts all paths which start with the given path.
func (p *Predict) paths(path string, includeFiles bool) []string {
client := p.Client()
if client == nil {
return nil
}
// Vault does not support listing based on a sub-key, so we have to back-pedal
// to the last "/" and return all paths on that "folder". Then we perform
// client-side filtering.
root := path
idx := strings.LastIndex(root, "/")
if idx > 0 && idx < len(root) {
root = root[:idx+1]
}
paths := p.listPaths(root)
var predictions []string
for _, p := range paths {
// Calculate the absolute "path" for matching.
p = root + p
if strings.HasPrefix(p, path) {
// Ensure this is a directory or we've asked to include files.
if includeFiles || strings.HasSuffix(p, "/") {
predictions = append(predictions, p)
}
}
}
// Add root to the path
if len(predictions) == 0 {
predictions = append(predictions, path)
}
return predictions
}
// audits returns a sorted list of the audit backends for Vault server for
// which the client is configured to communicate with.
func (p *Predict) audits() []string {
client := p.Client()
if client == nil {
return nil
}
audits, err := client.Sys().ListAudit()
if err != nil {
return nil
}
list := make([]string, 0, len(audits))
for m := range audits {
list = append(list, m)
}
sort.Strings(list)
return list
}
// auths returns a sorted list of the enabled auth provides for Vault server for
// which the client is configured to communicate with.
func (p *Predict) auths() []string {
client := p.Client()
if client == nil {
return nil
}
auths, err := client.Sys().ListAuth()
if err != nil {
return nil
}
list := make([]string, 0, len(auths))
for m := range auths {
list = append(list, m)
}
sort.Strings(list)
return list
}
// policies returns a sorted list of the policies stored in this Vault
// server.
func (p *Predict) policies() []string {
client := p.Client()
if client == nil {
return nil
}
policies, err := client.Sys().ListPolicies()
if err != nil {
return nil
}
sort.Strings(policies)
return policies
}
// mounts returns a sorted list of the mount paths for Vault server for
// which the client is configured to communicate with. This function returns the
// default list of mounts if an error occurs.
func (p *Predict) mounts() []string {
client := p.Client()
if client == nil {
return nil
}
mounts, err := client.Sys().ListMounts()
if err != nil {
return defaultPredictVaultMounts
}
list := make([]string, 0, len(mounts))
for m := range mounts {
list = append(list, m)
}
sort.Strings(list)
return list
}
// listPaths returns a list of paths (HTTP LIST) for the given path. This
// function returns an empty list of any errors occur.
func (p *Predict) listPaths(path string) []string {
client := p.Client()
if client == nil {
return nil
}
secret, err := client.Logical().List(path)
if err != nil || secret == nil || secret.Data == nil {
return nil
}
paths, ok := secret.Data["keys"].([]interface{})
if !ok {
return nil
}
list := make([]string, 0, len(paths))
for _, p := range paths {
if str, ok := p.(string); ok {
list = append(list, str)
}
}
sort.Strings(list)
return list
}
// hasPathArg determines if the args have already accepted a path.
func (p *Predict) hasPathArg(args []string) bool {
var nonFlags []string
for _, a := range args {
if !strings.HasPrefix(a, "-") {
nonFlags = append(nonFlags, a)
}
}
return len(nonFlags) > 2
}
// filterFunc is used to compose a complete predictor that filters an array
// of strings as per the filter function.
func (p *Predict) filterFunc(f func() []string) complete.Predictor {
return complete.PredictFunc(func(args complete.Args) []string {
if p.hasPathArg(args.All) {
return nil
}
client := p.Client()
if client == nil {
return nil
}
return p.filter(f(), args.Last)
})
}
// filter filters the given list for items that start with the prefix.
func (p *Predict) filter(list []string, prefix string) []string {
var predictions []string
for _, item := range list {
if strings.HasPrefix(item, prefix) {
predictions = append(predictions, item)
}
}
return predictions
}

View file

@ -0,0 +1,526 @@
package command
import (
"reflect"
"testing"
"github.com/hashicorp/vault/api"
"github.com/posener/complete"
)
func TestPredictVaultPaths(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
data := map[string]interface{}{"a": "b"}
if _, err := client.Logical().Write("secret/bar", data); err != nil {
t.Fatal(err)
}
if _, err := client.Logical().Write("secret/foo", data); err != nil {
t.Fatal(err)
}
if _, err := client.Logical().Write("secret/zip/zap", data); err != nil {
t.Fatal(err)
}
if _, err := client.Logical().Write("secret/zip/zonk", data); err != nil {
t.Fatal(err)
}
if _, err := client.Logical().Write("secret/zip/twoot", data); err != nil {
t.Fatal(err)
}
cases := []struct {
name string
args complete.Args
includeFiles bool
exp []string
}{
{
"has_args",
complete.Args{
All: []string{"read", "secret/foo", "a=b"},
Last: "a=b",
},
true,
nil,
},
{
"has_args_no_files",
complete.Args{
All: []string{"read", "secret/foo", "a=b"},
Last: "a=b",
},
false,
nil,
},
{
"part_mount",
complete.Args{
All: []string{"read", "s"},
Last: "s",
},
true,
[]string{"secret/", "sys/"},
},
{
"part_mount_no_files",
complete.Args{
All: []string{"read", "s"},
Last: "s",
},
false,
[]string{"secret/", "sys/"},
},
{
"only_mount",
complete.Args{
All: []string{"read", "sec"},
Last: "sec",
},
true,
[]string{"secret/bar", "secret/foo", "secret/zip/"},
},
{
"only_mount_no_files",
complete.Args{
All: []string{"read", "sec"},
Last: "sec",
},
false,
[]string{"secret/zip/"},
},
{
"full_mount",
complete.Args{
All: []string{"read", "secret"},
Last: "secret",
},
true,
[]string{"secret/bar", "secret/foo", "secret/zip/"},
},
{
"full_mount_no_files",
complete.Args{
All: []string{"read", "secret"},
Last: "secret",
},
false,
[]string{"secret/zip/"},
},
{
"full_mount_slash",
complete.Args{
All: []string{"read", "secret/"},
Last: "secret/",
},
true,
[]string{"secret/bar", "secret/foo", "secret/zip/"},
},
{
"full_mount_slash_no_files",
complete.Args{
All: []string{"read", "secret/"},
Last: "secret/",
},
false,
[]string{"secret/zip/"},
},
{
"path_partial",
complete.Args{
All: []string{"read", "secret/z"},
Last: "secret/z",
},
true,
[]string{"secret/zip/twoot", "secret/zip/zap", "secret/zip/zonk"},
},
{
"path_partial_no_files",
complete.Args{
All: []string{"read", "secret/z"},
Last: "secret/z",
},
false,
[]string{"secret/zip/"},
},
{
"subpath_partial_z",
complete.Args{
All: []string{"read", "secret/zip/z"},
Last: "secret/zip/z",
},
true,
[]string{"secret/zip/zap", "secret/zip/zonk"},
},
{
"subpath_partial_z_no_files",
complete.Args{
All: []string{"read", "secret/zip/z"},
Last: "secret/zip/z",
},
false,
[]string{"secret/zip/z"},
},
{
"subpath_partial_t",
complete.Args{
All: []string{"read", "secret/zip/t"},
Last: "secret/zip/t",
},
true,
[]string{"secret/zip/twoot"},
},
{
"subpath_partial_t_no_files",
complete.Args{
All: []string{"read", "secret/zip/t"},
Last: "secret/zip/t",
},
false,
[]string{"secret/zip/t"},
},
}
t.Run("group", func(t *testing.T) {
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
p := NewPredict()
p.client = client
f := p.vaultPaths(tc.includeFiles)
act := f(tc.args)
if !reflect.DeepEqual(act, tc.exp) {
t.Errorf("expected %q to be %q", act, tc.exp)
}
})
}
})
}
func TestPredict_Audits(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
badClient, badCloser := testVaultServerBad(t)
defer badCloser()
if err := client.Sys().EnableAuditWithOptions("file", &api.EnableAuditOptions{
Type: "file",
Options: map[string]string{
"file_path": "discard",
},
}); err != nil {
t.Fatal(err)
}
cases := []struct {
name string
client *api.Client
exp []string
}{
{
"not_connected_client",
badClient,
nil,
},
{
"good_path",
client,
[]string{"file/"},
},
}
t.Run("group", func(t *testing.T) {
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
p := NewPredict()
p.client = tc.client
act := p.audits()
if !reflect.DeepEqual(act, tc.exp) {
t.Errorf("expected %q to be %q", act, tc.exp)
}
})
}
})
}
func TestPredict_Mounts(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
badClient, badCloser := testVaultServerBad(t)
defer badCloser()
cases := []struct {
name string
client *api.Client
exp []string
}{
{
"not_connected_client",
badClient,
defaultPredictVaultMounts,
},
{
"good_path",
client,
[]string{"cubbyhole/", "identity/", "secret/", "sys/"},
},
}
t.Run("group", func(t *testing.T) {
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
p := NewPredict()
p.client = tc.client
act := p.mounts()
if !reflect.DeepEqual(act, tc.exp) {
t.Errorf("expected %q to be %q", act, tc.exp)
}
})
}
})
}
func TestPredict_Policies(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
badClient, badCloser := testVaultServerBad(t)
defer badCloser()
cases := []struct {
name string
client *api.Client
exp []string
}{
{
"not_connected_client",
badClient,
nil,
},
{
"good_path",
client,
[]string{"default", "root"},
},
}
t.Run("group", func(t *testing.T) {
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
p := NewPredict()
p.client = tc.client
act := p.policies()
if !reflect.DeepEqual(act, tc.exp) {
t.Errorf("expected %q to be %q", act, tc.exp)
}
})
}
})
}
func TestPredict_Paths(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
data := map[string]interface{}{"a": "b"}
if _, err := client.Logical().Write("secret/bar", data); err != nil {
t.Fatal(err)
}
if _, err := client.Logical().Write("secret/foo", data); err != nil {
t.Fatal(err)
}
if _, err := client.Logical().Write("secret/zip/zap", data); err != nil {
t.Fatal(err)
}
cases := []struct {
name string
path string
includeFiles bool
exp []string
}{
{
"bad_path",
"nope/not/a/real/path/ever",
true,
[]string{"nope/not/a/real/path/ever"},
},
{
"good_path",
"secret/",
true,
[]string{"secret/bar", "secret/foo", "secret/zip/"},
},
{
"good_path_no_files",
"secret/",
false,
[]string{"secret/zip/"},
},
{
"partial_match",
"secret/z",
true,
[]string{"secret/zip/"},
},
{
"partial_match_no_files",
"secret/z",
false,
[]string{"secret/zip/"},
},
}
t.Run("group", func(t *testing.T) {
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
p := NewPredict()
p.client = client
act := p.paths(tc.path, tc.includeFiles)
if !reflect.DeepEqual(act, tc.exp) {
t.Errorf("expected %q to be %q", act, tc.exp)
}
})
}
})
}
func TestPredict_ListPaths(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
badClient, badCloser := testVaultServerBad(t)
defer badCloser()
data := map[string]interface{}{"a": "b"}
if _, err := client.Logical().Write("secret/bar", data); err != nil {
t.Fatal(err)
}
if _, err := client.Logical().Write("secret/foo", data); err != nil {
t.Fatal(err)
}
cases := []struct {
name string
client *api.Client
path string
exp []string
}{
{
"bad_path",
client,
"nope/not/a/real/path/ever",
nil,
},
{
"good_path",
client,
"secret/",
[]string{"bar", "foo"},
},
{
"not_connected_client",
badClient,
"secret/",
nil,
},
}
t.Run("group", func(t *testing.T) {
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
p := NewPredict()
p.client = tc.client
act := p.listPaths(tc.path)
if !reflect.DeepEqual(act, tc.exp) {
t.Errorf("expected %q to be %q", act, tc.exp)
}
})
}
})
}
func TestPredict_HasPathArg(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
exp bool
}{
{
"nil",
nil,
false,
},
{
"empty",
[]string{},
false,
},
{
"empty_string",
[]string{""},
false,
},
{
"single",
[]string{"foo"},
false,
},
{
"multiple",
[]string{"foo", "bar", "baz"},
true,
},
}
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
p := NewPredict()
if act := p.hasPathArg(tc.args); act != tc.exp {
t.Errorf("expected %t to be %t", act, tc.exp)
}
})
}
}

View file

@ -1,87 +0,0 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/vault/meta"
)
// CapabilitiesCommand is a Command that enables a new endpoint.
type CapabilitiesCommand struct {
meta.Meta
}
func (c *CapabilitiesCommand) Run(args []string) int {
flags := c.Meta.FlagSet("capabilities", meta.FlagSetDefault)
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) > 2 {
flags.Usage()
c.Ui.Error(fmt.Sprintf(
"\ncapabilities expects at most two arguments"))
return 1
}
var token string
var path string
switch {
case len(args) == 1:
path = args[0]
case len(args) == 2:
token = args[0]
path = args[1]
default:
flags.Usage()
c.Ui.Error(fmt.Sprintf("\ncapabilities expects at least one argument"))
return 1
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 2
}
var capabilities []string
if token == "" {
capabilities, err = client.Sys().CapabilitiesSelf(path)
} else {
capabilities, err = client.Sys().Capabilities(token, path)
}
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error retrieving capabilities: %s", err))
return 1
}
c.Ui.Output(fmt.Sprintf("Capabilities: %s", capabilities))
return 0
}
func (c *CapabilitiesCommand) Synopsis() string {
return "Fetch the capabilities of a token on a given path"
}
func (c *CapabilitiesCommand) Help() string {
helpText := `
Usage: vault capabilities [options] [token] path
Fetch the capabilities of a token on a given path.
If a token is provided as an argument, the '/sys/capabilities' endpoint will be invoked
with the given token; otherwise the '/sys/capabilities-self' endpoint will be invoked
with the client token.
If a token does not have any capability on a given path, or if any of the policies
belonging to the token explicitly have ["deny"] capability, or if the argument path
is invalid, this command will respond with a ["deny"].
General Options:
` + meta.GeneralOptionsUsage()
return strings.TrimSpace(helpText)
}

View file

@ -1,45 +0,0 @@
package command
import (
"testing"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func TestCapabilities_Basic(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &CapabilitiesCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
},
}
var args []string
args = []string{"-address", addr}
if code := c.Run(args); code == 0 {
t.Fatalf("expected failure due to no args")
}
args = []string{"-address", addr, "testpath"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
args = []string{"-address", addr, token, "test"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
args = []string{"-address", addr, "invalidtoken", "test"}
if code := c.Run(args); code == 0 {
t.Fatalf("expected failure due to invalid token")
}
}

View file

@ -1,17 +1,212 @@
package command
import (
"context"
"encoding/base64"
"net"
"net/http"
"strings"
"testing"
"time"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/audit"
"github.com/hashicorp/vault/builtin/logical/pki"
"github.com/hashicorp/vault/builtin/logical/ssh"
"github.com/hashicorp/vault/builtin/logical/transit"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/physical/inmem"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
auditFile "github.com/hashicorp/vault/builtin/audit/file"
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
vaulthttp "github.com/hashicorp/vault/http"
logxi "github.com/mgutz/logxi/v1"
)
func testClient(t *testing.T, addr string, token string) *api.Client {
var (
defaultVaultLogger = logxi.NullLog
defaultVaultCredentialBackends = map[string]logical.Factory{
"userpass": credUserpass.Factory,
}
defaultVaultAuditBackends = map[string]audit.Factory{
"file": auditFile.Factory,
}
defaultVaultLogicalBackends = map[string]logical.Factory{
"generic-leased": vault.LeasedPassthroughBackendFactory,
"pki": pki.Factory,
"ssh": ssh.Factory,
"transit": transit.Factory,
}
)
// assertNoTabs asserts the CLI help has no tab characters.
func assertNoTabs(tb testing.TB, c cli.Command) {
tb.Helper()
if strings.ContainsRune(c.Help(), '\t') {
tb.Errorf("%#v help output contains tabs", c)
}
}
// testVaultServer creates a test vault cluster and returns a configured API
// client and closer function.
func testVaultServer(tb testing.TB) (*api.Client, func()) {
tb.Helper()
client, _, closer := testVaultServerUnseal(tb)
return client, closer
}
// testVaultServerUnseal creates a test vault cluster and returns a configured
// API client, list of unseal keys (as strings), and a closer function.
func testVaultServerUnseal(tb testing.TB) (*api.Client, []string, func()) {
tb.Helper()
return testVaultServerCoreConfig(tb, &vault.CoreConfig{
DisableMlock: true,
DisableCache: true,
Logger: defaultVaultLogger,
CredentialBackends: defaultVaultCredentialBackends,
AuditBackends: defaultVaultAuditBackends,
LogicalBackends: defaultVaultLogicalBackends,
})
}
// testVaultServerCoreConfig creates a new vault cluster with the given core
// configuration. This is a lower-level test helper.
func testVaultServerCoreConfig(tb testing.TB, coreConfig *vault.CoreConfig) (*api.Client, []string, func()) {
tb.Helper()
cluster := vault.NewTestCluster(tb, coreConfig, &vault.TestClusterOptions{
HandlerFunc: vaulthttp.Handler,
NumCores: 1, // Default is 3, but we don't need that many
})
cluster.Start()
// Make it easy to get access to the active
core := cluster.Cores[0].Core
vault.TestWaitActive(tb, core)
// Get the client already setup for us!
client := cluster.Cores[0].Client
client.SetToken(cluster.RootToken)
// Convert the unseal keys to base64 encoded, since these are how the user
// will get them.
unsealKeys := make([]string, len(cluster.BarrierKeys))
for i := range unsealKeys {
unsealKeys[i] = base64.StdEncoding.EncodeToString(cluster.BarrierKeys[i])
}
return client, unsealKeys, func() { defer cluster.Cleanup() }
}
// testVaultServerUninit creates an uninitialized server.
func testVaultServerUninit(tb testing.TB) (*api.Client, func()) {
tb.Helper()
inm, err := inmem.NewInmem(nil, defaultVaultLogger)
if err != nil {
tb.Fatal(err)
}
core, err := vault.NewCore(&vault.CoreConfig{
DisableMlock: true,
DisableCache: true,
Logger: defaultVaultLogger,
Physical: inm,
CredentialBackends: defaultVaultCredentialBackends,
AuditBackends: defaultVaultAuditBackends,
LogicalBackends: defaultVaultLogicalBackends,
})
if err != nil {
tb.Fatal(err)
}
ln, addr := vaulthttp.TestServer(tb, core)
client, err := api.NewClient(&api.Config{
Address: addr,
})
if err != nil {
tb.Fatal(err)
}
return client, func() { ln.Close() }
}
// testVaultServerBad creates an http server that returns a 500 on each request
// to simulate failures.
func testVaultServerBad(tb testing.TB) (*api.Client, func()) {
tb.Helper()
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
tb.Fatal(err)
}
server := &http.Server{
Addr: "127.0.0.1:0",
Handler: http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.Error(w, "500 internal server error", http.StatusInternalServerError)
}),
ReadTimeout: 1 * time.Second,
ReadHeaderTimeout: 1 * time.Second,
WriteTimeout: 1 * time.Second,
IdleTimeout: 1 * time.Second,
}
go func() {
if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
tb.Fatal(err)
}
}()
client, err := api.NewClient(&api.Config{
Address: "http://" + listener.Addr().String(),
})
if err != nil {
tb.Fatal(err)
}
return client, func() {
ctx, done := context.WithTimeout(context.Background(), 5*time.Second)
defer done()
server.Shutdown(ctx)
}
}
// testTokenAndAccessor creates a new authentication token capable of being renewed with
// the default policy attached. It returns the token and it's accessor.
func testTokenAndAccessor(tb testing.TB, client *api.Client) (string, string) {
tb.Helper()
secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{
Policies: []string{"default"},
TTL: "30m",
})
if err != nil {
tb.Fatal(err)
}
if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" {
tb.Fatalf("missing auth data: %#v", secret)
}
return secret.Auth.ClientToken, secret.Auth.Accessor
}
func testClient(tb testing.TB, addr string, token string) *api.Client {
tb.Helper()
config := api.DefaultConfig()
config.Address = addr
client, err := api.NewClient(config)
if err != nil {
t.Fatalf("err: %s", err)
tb.Fatal(err)
}
client.SetToken(token)

960
command/commands.go Normal file
View file

@ -0,0 +1,960 @@
package command
import (
"fmt"
"os"
"os/signal"
"syscall"
"github.com/hashicorp/vault/audit"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/physical"
"github.com/hashicorp/vault/version"
"github.com/mitchellh/cli"
"github.com/hashicorp/vault/builtin/logical/aws"
"github.com/hashicorp/vault/builtin/logical/cassandra"
"github.com/hashicorp/vault/builtin/logical/consul"
"github.com/hashicorp/vault/builtin/logical/database"
"github.com/hashicorp/vault/builtin/logical/mongodb"
"github.com/hashicorp/vault/builtin/logical/mssql"
"github.com/hashicorp/vault/builtin/logical/mysql"
"github.com/hashicorp/vault/builtin/logical/pki"
"github.com/hashicorp/vault/builtin/logical/postgresql"
"github.com/hashicorp/vault/builtin/logical/rabbitmq"
"github.com/hashicorp/vault/builtin/logical/ssh"
"github.com/hashicorp/vault/builtin/logical/totp"
"github.com/hashicorp/vault/builtin/logical/transit"
"github.com/hashicorp/vault/builtin/plugin"
auditFile "github.com/hashicorp/vault/builtin/audit/file"
auditSocket "github.com/hashicorp/vault/builtin/audit/socket"
auditSyslog "github.com/hashicorp/vault/builtin/audit/syslog"
credGcp "github.com/hashicorp/vault-plugin-auth-gcp/plugin"
credKube "github.com/hashicorp/vault-plugin-auth-kubernetes"
credAppId "github.com/hashicorp/vault/builtin/credential/app-id"
credAppRole "github.com/hashicorp/vault/builtin/credential/approle"
credAws "github.com/hashicorp/vault/builtin/credential/aws"
credCert "github.com/hashicorp/vault/builtin/credential/cert"
credGitHub "github.com/hashicorp/vault/builtin/credential/github"
credLdap "github.com/hashicorp/vault/builtin/credential/ldap"
credOkta "github.com/hashicorp/vault/builtin/credential/okta"
credRadius "github.com/hashicorp/vault/builtin/credential/radius"
credToken "github.com/hashicorp/vault/builtin/credential/token"
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
physAzure "github.com/hashicorp/vault/physical/azure"
physCassandra "github.com/hashicorp/vault/physical/cassandra"
physCockroachDB "github.com/hashicorp/vault/physical/cockroachdb"
physConsul "github.com/hashicorp/vault/physical/consul"
physCouchDB "github.com/hashicorp/vault/physical/couchdb"
physDynamoDB "github.com/hashicorp/vault/physical/dynamodb"
physEtcd "github.com/hashicorp/vault/physical/etcd"
physFile "github.com/hashicorp/vault/physical/file"
physGCS "github.com/hashicorp/vault/physical/gcs"
physInmem "github.com/hashicorp/vault/physical/inmem"
physMSSQL "github.com/hashicorp/vault/physical/mssql"
physMySQL "github.com/hashicorp/vault/physical/mysql"
physPostgreSQL "github.com/hashicorp/vault/physical/postgresql"
physS3 "github.com/hashicorp/vault/physical/s3"
physSwift "github.com/hashicorp/vault/physical/swift"
physZooKeeper "github.com/hashicorp/vault/physical/zookeeper"
)
// DeprecatedCommand is a command that wraps an existing command and prints a
// deprecation notice and points the user to the new command. Deprecated
// commands are always hidden from help output.
type DeprecatedCommand struct {
cli.Command
UI cli.Ui
// Old is the old command name, New is the new command name.
Old, New string
}
// Help wraps the embedded Help command and prints a warning about deprecations.
func (c *DeprecatedCommand) Help() string {
c.warn()
return c.Command.Help()
}
// Run wraps the embedded Run command and prints a warning about deprecation.
func (c *DeprecatedCommand) Run(args []string) int {
c.warn()
return c.Command.Run(args)
}
func (c *DeprecatedCommand) warn() {
c.UI.Warn(wrapAtLength(fmt.Sprintf(
"WARNING! The \"vault %s\" command is deprecated. Please use \"vault %s\" "+
"instead. This command will be removed in the next major release of "+
"Vault.",
c.Old,
c.New)))
c.UI.Warn("")
}
// Commands is the mapping of all the available commands.
var Commands map[string]cli.CommandFactory
var DeprecatedCommands map[string]cli.CommandFactory
func init() {
ui := &cli.ColoredUi{
ErrorColor: cli.UiColorRed,
WarnColor: cli.UiColorYellow,
Ui: &cli.BasicUi{
Writer: os.Stdout,
ErrorWriter: os.Stderr,
},
}
loginHandlers := map[string]LoginHandler{
"aws": &credAws.CLIHandler{},
"cert": &credCert.CLIHandler{},
"github": &credGitHub.CLIHandler{},
"ldap": &credLdap.CLIHandler{},
"okta": &credOkta.CLIHandler{},
"radius": &credUserpass.CLIHandler{
DefaultMount: "radius",
},
"token": &credToken.CLIHandler{},
"userpass": &credUserpass.CLIHandler{
DefaultMount: "userpass",
},
}
Commands = map[string]cli.CommandFactory{
"audit": func() (cli.Command, error) {
return &AuditCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"audit disable": func() (cli.Command, error) {
return &AuditDisableCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"audit enable": func() (cli.Command, error) {
return &AuditEnableCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"audit list": func() (cli.Command, error) {
return &AuditListCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"auth tune": func() (cli.Command, error) {
return &AuthTuneCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"auth": func() (cli.Command, error) {
return &AuthCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
Handlers: loginHandlers,
}, nil
},
"auth disable": func() (cli.Command, error) {
return &AuthDisableCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"auth enable": func() (cli.Command, error) {
return &AuthEnableCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"auth help": func() (cli.Command, error) {
return &AuthHelpCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
Handlers: loginHandlers,
}, nil
},
"auth list": func() (cli.Command, error) {
return &AuthListCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"delete": func() (cli.Command, error) {
return &DeleteCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"lease": func() (cli.Command, error) {
return &LeaseCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"lease renew": func() (cli.Command, error) {
return &LeaseRenewCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"lease revoke": func() (cli.Command, error) {
return &LeaseRevokeCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"list": func() (cli.Command, error) {
return &ListCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"login": func() (cli.Command, error) {
return &LoginCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
Handlers: loginHandlers,
}, nil
},
"operator": func() (cli.Command, error) {
return &OperatorCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"operator generate-root": func() (cli.Command, error) {
return &OperatorGenerateRootCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"operator init": func() (cli.Command, error) {
return &OperatorInitCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"operator key-status": func() (cli.Command, error) {
return &OperatorKeyStatusCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"operator rekey": func() (cli.Command, error) {
return &OperatorRekeyCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"operator rotate": func() (cli.Command, error) {
return &OperatorRotateCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"operator seal": func() (cli.Command, error) {
return &OperatorSealCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"operator step-down": func() (cli.Command, error) {
return &OperatorStepDownCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"operator unseal": func() (cli.Command, error) {
return &OperatorUnsealCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"path-help": func() (cli.Command, error) {
return &PathHelpCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"policy": func() (cli.Command, error) {
return &PolicyCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"policy delete": func() (cli.Command, error) {
return &PolicyDeleteCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"policy fmt": func() (cli.Command, error) {
return &PolicyFmtCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"policy list": func() (cli.Command, error) {
return &PolicyListCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"policy read": func() (cli.Command, error) {
return &PolicyReadCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"policy write": func() (cli.Command, error) {
return &PolicyWriteCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"read": func() (cli.Command, error) {
return &ReadCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"secrets": func() (cli.Command, error) {
return &SecretsCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"secrets disable": func() (cli.Command, error) {
return &SecretsDisableCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"secrets enable": func() (cli.Command, error) {
return &SecretsEnableCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"secrets list": func() (cli.Command, error) {
return &SecretsListCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"secrets move": func() (cli.Command, error) {
return &SecretsMoveCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"secrets tune": func() (cli.Command, error) {
return &SecretsTuneCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"server": func() (cli.Command, error) {
return &ServerCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
AuditBackends: map[string]audit.Factory{
"file": auditFile.Factory,
"socket": auditSocket.Factory,
"syslog": auditSyslog.Factory,
},
CredentialBackends: map[string]logical.Factory{
"app-id": credAppId.Factory,
"approle": credAppRole.Factory,
"aws": credAws.Factory,
"cert": credCert.Factory,
"gcp": credGcp.Factory,
"github": credGitHub.Factory,
"kubernetes": credKube.Factory,
"ldap": credLdap.Factory,
"okta": credOkta.Factory,
"plugin": plugin.Factory,
"radius": credRadius.Factory,
"userpass": credUserpass.Factory,
},
LogicalBackends: map[string]logical.Factory{
"aws": aws.Factory,
"cassandra": cassandra.Factory,
"consul": consul.Factory,
"database": database.Factory,
"mongodb": mongodb.Factory,
"mssql": mssql.Factory,
"mysql": mysql.Factory,
"pki": pki.Factory,
"plugin": plugin.Factory,
"postgresql": postgresql.Factory,
"rabbitmq": rabbitmq.Factory,
"ssh": ssh.Factory,
"totp": totp.Factory,
"transit": transit.Factory,
},
PhysicalBackends: map[string]physical.Factory{
"azure": physAzure.NewAzureBackend,
"cassandra": physCassandra.NewCassandraBackend,
"cockroachdb": physCockroachDB.NewCockroachDBBackend,
"consul": physConsul.NewConsulBackend,
"couchdb_transactional": physCouchDB.NewTransactionalCouchDBBackend,
"couchdb": physCouchDB.NewCouchDBBackend,
"dynamodb": physDynamoDB.NewDynamoDBBackend,
"etcd": physEtcd.NewEtcdBackend,
"file_transactional": physFile.NewTransactionalFileBackend,
"file": physFile.NewFileBackend,
"gcs": physGCS.NewGCSBackend,
"inmem_ha": physInmem.NewInmemHA,
"inmem_transactional_ha": physInmem.NewTransactionalInmemHA,
"inmem_transactional": physInmem.NewTransactionalInmem,
"inmem": physInmem.NewInmem,
"mssql": physMSSQL.NewMSSQLBackend,
"mysql": physMySQL.NewMySQLBackend,
"postgresql": physPostgreSQL.NewPostgreSQLBackend,
"s3": physS3.NewS3Backend,
"swift": physSwift.NewSwiftBackend,
"zookeeper": physZooKeeper.NewZooKeeperBackend,
},
ShutdownCh: MakeShutdownCh(),
SighupCh: MakeSighupCh(),
}, nil
},
"ssh": func() (cli.Command, error) {
return &SSHCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"status": func() (cli.Command, error) {
return &StatusCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"token": func() (cli.Command, error) {
return &TokenCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"token create": func() (cli.Command, error) {
return &TokenCreateCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"token capabilities": func() (cli.Command, error) {
return &TokenCapabilitiesCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"token lookup": func() (cli.Command, error) {
return &TokenLookupCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"token renew": func() (cli.Command, error) {
return &TokenRenewCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"token revoke": func() (cli.Command, error) {
return &TokenRevokeCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"unwrap": func() (cli.Command, error) {
return &UnwrapCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"version": func() (cli.Command, error) {
return &VersionCommand{
VersionInfo: version.GetVersion(),
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
"write": func() (cli.Command, error) {
return &WriteCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}, nil
},
}
// Deprecated commands
//
// TODO: Remove in 0.9.0
DeprecatedCommands = map[string]cli.CommandFactory{
"audit-disable": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "audit-disable",
New: "audit disable",
UI: ui,
Command: &AuditDisableCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"audit-enable": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "audit-enable",
New: "audit enable",
UI: ui,
Command: &AuditEnableCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"audit-list": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "audit-list",
New: "audit list",
UI: ui,
Command: &AuditListCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"auth-disable": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "auth-disable",
New: "auth disable",
UI: ui,
Command: &AuthDisableCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"auth-enable": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "auth-enable",
New: "auth enable",
UI: ui,
Command: &AuthEnableCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"capabilities": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "capabilities",
New: "token capabilities",
UI: ui,
Command: &TokenCapabilitiesCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"generate-root": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "generate-root",
New: "operator generate-root",
UI: ui,
Command: &OperatorGenerateRootCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"init": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "init",
New: "operator init",
UI: ui,
Command: &OperatorInitCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"key-status": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "key-status",
New: "operator key-status",
UI: ui,
Command: &OperatorKeyStatusCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"renew": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "renew",
New: "lease renew",
UI: ui,
Command: &LeaseRenewCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"revoke": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "revoke",
New: "lease revoke",
UI: ui,
Command: &LeaseRevokeCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"mount": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "mount",
New: "secrets enable",
UI: ui,
Command: &SecretsEnableCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"mount-tune": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "mount-tune",
New: "secrets tune",
UI: ui,
Command: &SecretsTuneCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"mounts": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "mounts",
New: "secrets list",
UI: ui,
Command: &SecretsListCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"policies": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "policies",
New: "policy read\" or \"vault policy list", // lol
UI: ui,
Command: &PoliciesDeprecatedCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"policy-delete": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "policy-delete",
New: "policy delete",
UI: ui,
Command: &PolicyDeleteCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"policy-write": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "policy-write",
New: "policy write",
UI: ui,
Command: &PolicyWriteCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"rekey": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "rekey",
New: "operator rekey",
UI: ui,
Command: &OperatorRekeyCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"remount": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "remount",
New: "secrets move",
UI: ui,
Command: &SecretsMoveCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"rotate": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "rotate",
New: "operator rotate",
UI: ui,
Command: &OperatorRotateCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"seal": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "seal",
New: "operator seal",
UI: ui,
Command: &OperatorSealCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"step-down": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "step-down",
New: "operator step-down",
UI: ui,
Command: &OperatorStepDownCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"token-create": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "token-create",
New: "token create",
UI: ui,
Command: &TokenCreateCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"token-lookup": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "token-lookup",
New: "token lookup",
UI: ui,
Command: &TokenLookupCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"token-renew": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "token-renew",
New: "token renew",
UI: ui,
Command: &TokenRenewCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"token-revoke": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "token-revoke",
New: "token revoke",
UI: ui,
Command: &TokenRevokeCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"unmount": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "unmount",
New: "secrets disable",
UI: ui,
Command: &SecretsDisableCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
"unseal": func() (cli.Command, error) {
return &DeprecatedCommand{
Old: "unseal",
New: "operator unseal",
UI: ui,
Command: &OperatorUnsealCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
},
}, nil
},
}
// Add deprecated commands back to the main commands so they parse.
for k, v := range DeprecatedCommands {
if _, ok := Commands[k]; ok {
// Can't deprecate an existing command...
panic(fmt.Sprintf("command %q defined as deprecated and not at the same time!", k))
}
Commands[k] = v
}
}
// MakeShutdownCh returns a channel that can be used for shutdown
// notifications for commands. This channel will send a message for every
// SIGINT or SIGTERM received.
func MakeShutdownCh() chan struct{} {
resultCh := make(chan struct{})
shutdownCh := make(chan os.Signal, 4)
signal.Notify(shutdownCh, os.Interrupt, syscall.SIGTERM)
go func() {
<-shutdownCh
close(resultCh)
}()
return resultCh
}
// MakeSighupCh returns a channel that can be used for SIGHUP
// reloading. This channel will send a message for every
// SIGHUP received.
func MakeSighupCh() chan struct{} {
resultCh := make(chan struct{})
signalCh := make(chan os.Signal, 4)
signal.Notify(signalCh, syscall.SIGHUP)
go func() {
for {
<-signalCh
resultCh <- struct{}{}
}
}()
return resultCh
}

View file

@ -4,64 +4,91 @@ import (
"fmt"
"strings"
"github.com/hashicorp/vault/meta"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
// DeleteCommand is a Command that puts data into the Vault.
var _ cli.Command = (*DeleteCommand)(nil)
var _ cli.CommandAutocomplete = (*DeleteCommand)(nil)
type DeleteCommand struct {
meta.Meta
}
func (c *DeleteCommand) Run(args []string) int {
flags := c.Meta.FlagSet("delete", meta.FlagSetDefault)
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) != 1 {
c.Ui.Error("delete expects one argument")
flags.Usage()
return 1
}
path := args[0]
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 2
}
if _, err := client.Logical().Delete(path); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error deleting '%s': %s", path, err))
return 1
}
c.Ui.Output(fmt.Sprintf("Success! Deleted '%s' if it existed.", path))
return 0
*BaseCommand
}
func (c *DeleteCommand) Synopsis() string {
return "Delete operation on secrets in Vault"
return "Delete secrets and configuration"
}
func (c *DeleteCommand) Help() string {
helpText := `
Usage: vault delete [options] path
Usage: vault delete [options] PATH
Delete data (secrets or configuration) from Vault.
Deletes secrets and configuration from Vault at the given path. The behavior
of "delete" is delegated to the backend corresponding to the given path.
Delete sends a delete operation request to the given path. The
behavior of the delete is determined by the backend at the given
path. For example, deleting "aws/policy/ops" will delete the "ops"
policy for the AWS backend. Use "vault help" for more details on
whether delete is supported for a path and what the behavior is.
Remove data in the status secret backend:
$ vault delete secret/my-secret
Uninstall an encryption key in the transit backend:
$ vault delete transit/keys/my-key
Delete an IAM role:
$ vault delete aws/roles/ops
For a full list of examples and paths, please see the documentation that
corresponds to the secret backend in use.
` + c.Flags().Help()
General Options:
` + meta.GeneralOptionsUsage()
return strings.TrimSpace(helpText)
}
func (c *DeleteCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP)
}
func (c *DeleteCommand) AutocompleteArgs() complete.Predictor {
return c.PredictVaultFiles()
}
func (c *DeleteCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *DeleteCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
path := sanitizePath(args[0])
if _, err := client.Logical().Delete(path); err != nil {
c.UI.Error(fmt.Sprintf("Error deleting %s: %s", path, err))
return 2
}
c.UI.Info(fmt.Sprintf("Success! Data deleted (if it existed) at: %s", path))
return 0
}

View file

@ -1,56 +1,131 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func TestDelete(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
func testDeleteCommand(tb testing.TB) (*cli.MockUi, *DeleteCommand) {
tb.Helper()
ui := new(cli.MockUi)
c := &DeleteCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
ui := cli.NewMockUi()
return ui, &DeleteCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestDeleteCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"not_enough_args",
[]string{},
"Not enough arguments",
1,
},
{
"too_many_args",
[]string{"foo", "bar"},
"Too many arguments",
1,
},
}
args := []string{
"-address", addr,
"secret/foo",
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
// Run once so the client is setup, ignore errors
c.Run(args)
for _, tc := range cases {
tc := tc
// Get the client so we can write data
client, err := c.Client()
if err != nil {
t.Fatalf("err: %s", err)
}
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
data := map[string]interface{}{"value": "bar"}
if _, err := client.Logical().Write("secret/foo", data); err != nil {
t.Fatalf("err: %s", err)
}
ui, cmd := testDeleteCommand(t)
// Run the delete
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
resp, err := client.Logical().Read("secret/foo")
if err != nil {
t.Fatalf("err: %s", err)
}
if resp != nil {
t.Fatalf("bad: %#v", resp)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("integration", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
if _, err := client.Logical().Write("secret/delete/foo", map[string]interface{}{
"foo": "bar",
}); err != nil {
t.Fatal(err)
}
ui, cmd := testDeleteCommand(t)
cmd.client = client
code := cmd.Run([]string{
"secret/delete/foo",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Success! Data deleted (if it existed) at: secret/delete/foo"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
secret, _ := client.Logical().Read("secret/delete/foo")
if secret != nil {
t.Errorf("expected deletion: %#v", secret)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testDeleteCommand(t)
cmd.client = client
code := cmd.Run([]string{
"secret/delete/foo",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error deleting secret/delete/foo: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testDeleteCommand(t)
assertNoTabs(t, cmd)
})
}

View file

@ -1,24 +1,23 @@
package command
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"sort"
"strconv"
"strings"
"sync"
"time"
"github.com/ghodss/yaml"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
"github.com/posener/complete"
"github.com/ryanuber/columnize"
)
var predictFormat complete.Predictor = complete.PredictSet("json", "yaml")
const (
// hopeDelim is the delimiter to use when splitting columns. We call it a
// hopeDelim because we hope that it's never contained in a secret.
hopeDelim = "♨"
)
func OutputSecret(ui cli.Ui, format string, secret *api.Secret) int {
return outputWithFormat(ui, format, secret, secret)
@ -29,6 +28,13 @@ func OutputList(ui cli.Ui, format string, secret *api.Secret) int {
}
func outputWithFormat(ui cli.Ui, format string, secret *api.Secret, data interface{}) int {
// If we had a colored UI, pull out the nested ui so we don't add escape
// sequences for outputting json, etc.
colorUI, ok := ui.(*cli.ColoredUi)
if ok {
ui = colorUI.Ui
}
formatter, ok := Formatters[strings.ToLower(format)]
if !ok {
ui.Error(fmt.Sprintf("Invalid output format: %s", format))
@ -53,17 +59,15 @@ var Formatters = map[string]Formatter{
}
// An output formatter for json output of an object
type JsonFormatter struct {
}
type JsonFormatter struct{}
func (j JsonFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) error {
b, err := json.Marshal(data)
if err == nil {
var out bytes.Buffer
json.Indent(&out, b, "", "\t")
ui.Output(out.String())
b, err := json.MarshalIndent(data, "", " ")
if err != nil {
return err
}
return err
ui.Output(string(b))
return nil
}
// An output formatter for yaml output format of an object
@ -85,7 +89,7 @@ type TableFormatter struct {
func (t TableFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{}) error {
// TODO: this should really use reflection like the other formatters do
if s, ok := data.(*api.Secret); ok {
return t.OutputSecret(ui, secret, s)
return t.OutputSecret(ui, s)
}
if s, ok := data.([]interface{}); ok {
return t.OutputList(ui, secret, s)
@ -94,133 +98,166 @@ func (t TableFormatter) Output(ui cli.Ui, secret *api.Secret, data interface{})
}
func (t TableFormatter) OutputList(ui cli.Ui, secret *api.Secret, list []interface{}) error {
config := columnize.DefaultConfig()
config.Delim = "♨"
config.Glue = "\t"
config.Prefix = ""
input := make([]string, 0, 5)
t.printWarnings(ui, secret)
if len(list) > 0 {
input = append(input, "Keys")
input = append(input, "----")
keys := make([]string, 0, len(list))
for _, k := range list {
keys = append(keys, k.(string))
keys := make([]string, len(list))
for i, v := range list {
typed, ok := v.(string)
if !ok {
return fmt.Errorf("Error: %v is not a string", v)
}
keys[i] = typed
}
sort.Strings(keys)
for _, k := range keys {
input = append(input, fmt.Sprintf("%s", k))
}
// Prepend the header
keys = append([]string{"Keys"}, keys...)
ui.Output(tableOutput(keys, &columnize.Config{
Delim: hopeDelim,
}))
}
tableOutputStr := columnize.Format(input, config)
// Print the warning separately because the length of first
// column in the output will be increased by the length of
// the longest warning string making the output look bad.
warningsInput := make([]string, 0, 5)
if len(secret.Warnings) != 0 {
warningsInput = append(warningsInput, "")
warningsInput = append(warningsInput, "The following warnings were returned from the Vault server:")
for _, warning := range secret.Warnings {
warningsInput = append(warningsInput, fmt.Sprintf("* %s", warning))
}
}
warningsOutputStr := columnize.Format(warningsInput, config)
ui.Output(fmt.Sprintf("%s\n%s", tableOutputStr, warningsOutputStr))
return nil
}
func (t TableFormatter) OutputSecret(ui cli.Ui, secret, s *api.Secret) error {
config := columnize.DefaultConfig()
config.Delim = "♨"
config.Glue = "\t"
config.Prefix = ""
// printWarnings prints any warnings in the secret.
func (t TableFormatter) printWarnings(ui cli.Ui, secret *api.Secret) {
if secret != nil && len(secret.Warnings) > 0 {
ui.Warn("WARNING! The following warnings were returned from Vault:\n")
for _, warning := range secret.Warnings {
ui.Warn(wrapAtLengthWithPadding(fmt.Sprintf("* %s", warning), 2))
}
ui.Warn("")
}
}
input := make([]string, 0, 5)
onceHeader := &sync.Once{}
headerFunc := func() {
input = append(input, fmt.Sprintf("Key %s Value", config.Delim))
input = append(input, fmt.Sprintf("--- %s -----", config.Delim))
func (t TableFormatter) OutputSecret(ui cli.Ui, secret *api.Secret) error {
if secret == nil {
return nil
}
if s.LeaseDuration > 0 {
onceHeader.Do(headerFunc)
if s.LeaseID != "" {
input = append(input, fmt.Sprintf("lease_id %s %s", config.Delim, s.LeaseID))
input = append(input, fmt.Sprintf(
"lease_duration %s %s", config.Delim, (time.Second*time.Duration(s.LeaseDuration)).String()))
t.printWarnings(ui, secret)
out := make([]string, 0, 8)
if secret.LeaseDuration > 0 {
if secret.LeaseID != "" {
out = append(out, fmt.Sprintf("lease_id %s %s", hopeDelim, secret.LeaseID))
out = append(out, fmt.Sprintf("lease_duration %s %s", hopeDelim, humanDurationInt(secret.LeaseDuration)))
out = append(out, fmt.Sprintf("lease_renewable %s %t", hopeDelim, secret.Renewable))
} else {
input = append(input, fmt.Sprintf(
"refresh_interval %s %s", config.Delim, (time.Second*time.Duration(s.LeaseDuration)).String()))
}
if s.LeaseID != "" {
input = append(input, fmt.Sprintf(
"lease_renewable %s %s", config.Delim, strconv.FormatBool(s.Renewable)))
// This is probably the generic secret backend which has leases, but we
// print them as refresh_interval to reduce confusion.
out = append(out, fmt.Sprintf("refresh_interval %s %s", hopeDelim, humanDurationInt(secret.LeaseDuration)))
}
}
if s.Auth != nil {
onceHeader.Do(headerFunc)
input = append(input, fmt.Sprintf("token %s %s", config.Delim, s.Auth.ClientToken))
input = append(input, fmt.Sprintf("token_accessor %s %s", config.Delim, s.Auth.Accessor))
input = append(input, fmt.Sprintf("token_duration %s %s", config.Delim, (time.Second*time.Duration(s.Auth.LeaseDuration)).String()))
input = append(input, fmt.Sprintf("token_renewable %s %v", config.Delim, s.Auth.Renewable))
input = append(input, fmt.Sprintf("token_policies %s %v", config.Delim, s.Auth.Policies))
for k, v := range s.Auth.Metadata {
input = append(input, fmt.Sprintf("token_meta_%s %s %#v", k, config.Delim, v))
if secret.Auth != nil {
out = append(out, fmt.Sprintf("token %s %s", hopeDelim, secret.Auth.ClientToken))
out = append(out, fmt.Sprintf("token_accessor %s %s", hopeDelim, secret.Auth.Accessor))
// If the lease duration is 0, it's likely a root token, so output the
// duration as "infinity" to clear things up.
if secret.Auth.LeaseDuration == 0 {
out = append(out, fmt.Sprintf("token_duration %s %s", hopeDelim, "∞"))
} else {
out = append(out, fmt.Sprintf("token_duration %s %s", hopeDelim, humanDurationInt(secret.Auth.LeaseDuration)))
}
out = append(out, fmt.Sprintf("token_renewable %s %t", hopeDelim, secret.Auth.Renewable))
out = append(out, fmt.Sprintf("token_policies %s %v", hopeDelim, secret.Auth.Policies))
for k, v := range secret.Auth.Metadata {
out = append(out, fmt.Sprintf("token_meta_%s %s %v", k, hopeDelim, v))
}
}
if s.WrapInfo != nil {
onceHeader.Do(headerFunc)
input = append(input, fmt.Sprintf("wrapping_token: %s %s", config.Delim, s.WrapInfo.Token))
input = append(input, fmt.Sprintf("wrapping_accessor: %s %s", config.Delim, s.WrapInfo.Accessor))
input = append(input, fmt.Sprintf("wrapping_token_ttl: %s %s", config.Delim, (time.Second*time.Duration(s.WrapInfo.TTL)).String()))
input = append(input, fmt.Sprintf("wrapping_token_creation_time: %s %s", config.Delim, s.WrapInfo.CreationTime.String()))
input = append(input, fmt.Sprintf("wrapping_token_creation_path: %s %s", config.Delim, s.WrapInfo.CreationPath))
if s.WrapInfo.WrappedAccessor != "" {
input = append(input, fmt.Sprintf("wrapped_accessor: %s %s", config.Delim, s.WrapInfo.WrappedAccessor))
if secret.WrapInfo != nil {
out = append(out, fmt.Sprintf("wrapping_token: %s %s", hopeDelim, secret.WrapInfo.Token))
out = append(out, fmt.Sprintf("wrapping_accessor: %s %s", hopeDelim, secret.WrapInfo.Accessor))
out = append(out, fmt.Sprintf("wrapping_token_ttl: %s %s", hopeDelim, humanDurationInt(secret.WrapInfo.TTL)))
out = append(out, fmt.Sprintf("wrapping_token_creation_time: %s %s", hopeDelim, secret.WrapInfo.CreationTime.String()))
out = append(out, fmt.Sprintf("wrapping_token_creation_path: %s %s", hopeDelim, secret.WrapInfo.CreationPath))
if secret.WrapInfo.WrappedAccessor != "" {
out = append(out, fmt.Sprintf("wrapped_accessor: %s %s", hopeDelim, secret.WrapInfo.WrappedAccessor))
}
}
if s.Data != nil && len(s.Data) > 0 {
onceHeader.Do(headerFunc)
keys := make([]string, 0, len(s.Data))
for k := range s.Data {
if len(secret.Data) > 0 {
keys := make([]string, 0, len(secret.Data))
for k := range secret.Data {
keys = append(keys, k)
}
sort.Strings(keys)
for _, k := range keys {
input = append(input, fmt.Sprintf("%s %s %v", k, config.Delim, s.Data[k]))
out = append(out, fmt.Sprintf("%s %s %v", k, hopeDelim, secret.Data[k]))
}
}
tableOutputStr := columnize.Format(input, config)
// Print the warning separately because the length of first
// column in the output will be increased by the length of
// the longest warning string making the output look bad.
warningsInput := make([]string, 0, 5)
if len(s.Warnings) != 0 {
warningsInput = append(warningsInput, "")
warningsInput = append(warningsInput, "The following warnings were returned from the Vault server:")
for _, warning := range s.Warnings {
warningsInput = append(warningsInput, fmt.Sprintf("* %s", warning))
}
// If we got this far and still don't have any data, there's nothing to print,
// sorry.
if len(out) == 0 {
return nil
}
warningsOutputStr := columnize.Format(warningsInput, config)
ui.Output(fmt.Sprintf("%s\n%s", tableOutputStr, warningsOutputStr))
// Prepend the header
out = append([]string{"Key" + hopeDelim + "Value"}, out...)
ui.Output(tableOutput(out, &columnize.Config{
Delim: hopeDelim,
}))
return nil
}
func OutputSealStatus(ui cli.Ui, client *api.Client, status *api.SealStatusResponse) int {
var sealPrefix string
if status.RecoverySeal {
sealPrefix = "Recovery "
}
out := []string{}
out = append(out, "Key | Value")
out = append(out, fmt.Sprintf("%sSeal Type | %s", sealPrefix, status.Type))
out = append(out, fmt.Sprintf("Sealed | %t", status.Sealed))
out = append(out, fmt.Sprintf("Total %sShares | %d", sealPrefix, status.N))
out = append(out, fmt.Sprintf("Threshold | %d", status.T))
if status.Sealed {
out = append(out, fmt.Sprintf("Unseal Progress | %d/%d", status.Progress, status.T))
out = append(out, fmt.Sprintf("Unseal Nonce | %s", status.Nonce))
}
out = append(out, fmt.Sprintf("Version | %s", status.Version))
if status.ClusterName != "" && status.ClusterID != "" {
out = append(out, fmt.Sprintf("Cluster Name | %s", status.ClusterName))
out = append(out, fmt.Sprintf("Cluster ID | %s", status.ClusterID))
}
// Mask the 'Vault is sealed' error, since this means HA is enabled, but that
// we cannot query for the leader since we are sealed.
leaderStatus, err := client.Sys().Leader()
if err != nil && strings.Contains(err.Error(), "Vault is sealed") {
leaderStatus = &api.LeaderResponse{HAEnabled: true}
}
// Output if HA is enabled
out = append(out, fmt.Sprintf("HA Enabled | %t", leaderStatus.HAEnabled))
if leaderStatus.HAEnabled {
mode := "sealed"
if !status.Sealed {
mode = "standby"
if leaderStatus.IsSelf {
mode = "active"
}
}
out = append(out, fmt.Sprintf("HA Mode | %s", mode))
if !status.Sealed {
out = append(out, fmt.Sprintf("HA Cluster | %s", leaderStatus.LeaderClusterAddress))
}
}
ui.Output(tableOutput(out, nil))
return 0
}

View file

@ -24,18 +24,10 @@ func (m mockUi) AskSecret(_ string) (string, error) {
m.t.FailNow()
return "", nil
}
func (m mockUi) Output(s string) {
output = s
}
func (m mockUi) Info(s string) {
m.t.Log(s)
}
func (m mockUi) Error(s string) {
m.t.Log(s)
}
func (m mockUi) Warn(s string) {
m.t.Log(s)
}
func (m mockUi) Output(s string) { output = s }
func (m mockUi) Info(s string) { m.t.Log(s) }
func (m mockUi) Error(s string) { m.t.Log(s) }
func (m mockUi) Warn(s string) { m.t.Log(s) }
func TestJsonFormatter(t *testing.T) {
ui := mockUi{t: t, SampleData: "something"}

View file

@ -1,405 +0,0 @@
package command
import (
"crypto/rand"
"encoding/base64"
"fmt"
"os"
"strings"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/password"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/helper/xor"
"github.com/hashicorp/vault/meta"
"github.com/posener/complete"
)
// GenerateRootCommand is a Command that generates a new root token.
type GenerateRootCommand struct {
meta.Meta
// Key can be used to pre-seed the key. If it is set, it will not
// be asked with the `password` helper.
Key string
// The nonce for the rekey request to send along
Nonce string
}
func (c *GenerateRootCommand) Run(args []string) int {
var init, cancel, status, genotp, drToken bool
var nonce, decode, otp, pgpKey string
var pgpKeyArr pgpkeys.PubKeyFilesFlag
flags := c.Meta.FlagSet("generate-root", meta.FlagSetDefault)
flags.BoolVar(&init, "init", false, "")
flags.BoolVar(&drToken, "dr-token", false, "")
flags.BoolVar(&cancel, "cancel", false, "")
flags.BoolVar(&status, "status", false, "")
flags.BoolVar(&genotp, "genotp", false, "")
flags.StringVar(&decode, "decode", "", "")
flags.StringVar(&otp, "otp", "", "")
flags.StringVar(&nonce, "nonce", "", "")
flags.Var(&pgpKeyArr, "pgp-key", "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
if genotp {
buf := make([]byte, 16)
readLen, err := rand.Read(buf)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading random bytes: %s", err))
return 1
}
if readLen != 16 {
c.Ui.Error(fmt.Sprintf("Read %d bytes when we should have read 16", readLen))
return 1
}
c.Ui.Output(fmt.Sprintf("OTP: %s", base64.StdEncoding.EncodeToString(buf)))
return 0
}
if len(decode) > 0 {
if len(otp) == 0 {
c.Ui.Error("Both the value to decode and the OTP must be passed in")
return 1
}
return c.decode(decode, otp)
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 2
}
// Check if the root generation is started
f := client.Sys().GenerateRootStatus
if drToken {
f = client.Sys().GenerateDROperationTokenStatus
}
rootGenerationStatus, err := f()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading root generation status: %s", err))
return 1
}
// If we are initing, or if we are not started but are not running a
// special function, check otp and pgpkey
checkOtpPgp := false
switch {
case init:
checkOtpPgp = true
case cancel:
case status:
case genotp:
case len(decode) != 0:
case rootGenerationStatus.Started:
default:
checkOtpPgp = true
}
if checkOtpPgp {
switch {
case len(otp) == 0 && (pgpKeyArr == nil || len(pgpKeyArr) == 0):
c.Ui.Error(c.Help())
return 1
case len(otp) != 0 && pgpKeyArr != nil && len(pgpKeyArr) != 0:
c.Ui.Error(c.Help())
return 1
case len(otp) != 0:
err := c.verifyOTP(otp)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error verifying the provided OTP: %s", err))
return 1
}
case pgpKeyArr != nil:
if len(pgpKeyArr) != 1 {
c.Ui.Error("Could not parse PGP key")
return 1
}
if len(pgpKeyArr[0]) == 0 {
c.Ui.Error("Got an empty PGP key")
return 1
}
pgpKey = pgpKeyArr[0]
default:
panic("unreachable case")
}
}
if nonce != "" {
c.Nonce = nonce
}
// Check if we are running doing any restricted variants
switch {
case init:
return c.initGenerateRoot(client, otp, pgpKey, drToken)
case cancel:
return c.cancelGenerateRoot(client, drToken)
case status:
return c.rootGenerationStatus(client, drToken)
}
// Start the root generation process if not started
if !rootGenerationStatus.Started {
f := client.Sys().GenerateRootInit
if drToken {
f = client.Sys().GenerateDROperationTokenInit
}
rootGenerationStatus, err = f(otp, pgpKey)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing root generation: %s", err))
return 1
}
c.Nonce = rootGenerationStatus.Nonce
}
serverNonce := rootGenerationStatus.Nonce
// Get the unseal key
args = flags.Args()
key := c.Key
if len(args) > 0 {
key = args[0]
}
if key == "" {
c.Nonce = serverNonce
fmt.Printf("Root generation operation nonce: %s\n", serverNonce)
fmt.Printf("Key (will be hidden): ")
key, err = password.Read(os.Stdin)
fmt.Printf("\n")
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error attempting to ask for password. The raw error message\n"+
"is shown below, but the most common reason for this error is\n"+
"that you attempted to pipe a value into unseal or you're\n"+
"executing `vault generate-root` from outside of a terminal.\n\n"+
"You should use `vault generate-root` from a terminal for maximum\n"+
"security. If this isn't an option, the unseal key can be passed\n"+
"in using the first parameter.\n\n"+
"Raw error: %s", err))
return 1
}
}
// Provide the key, this may potentially complete the update
{
f := client.Sys().GenerateRootUpdate
if drToken {
f = client.Sys().GenerateDROperationTokenUpdate
}
statusResp, err := f(strings.TrimSpace(key), c.Nonce)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error attempting generate-root update: %s", err))
return 1
}
c.dumpStatus(statusResp)
}
return 0
}
func (c *GenerateRootCommand) verifyOTP(otp string) error {
if len(otp) == 0 {
return fmt.Errorf("No OTP passed in")
}
otpBytes, err := base64.StdEncoding.DecodeString(otp)
if err != nil {
return fmt.Errorf("Error decoding base64 OTP value: %s", err)
}
if otpBytes == nil || len(otpBytes) != 16 {
return fmt.Errorf("Decoded OTP value is invalid or wrong length")
}
return nil
}
func (c *GenerateRootCommand) decode(encodedVal, otp string) int {
tokenBytes, err := xor.XORBase64(encodedVal, otp)
if err != nil {
c.Ui.Error(err.Error())
return 1
}
token, err := uuid.FormatUUID(tokenBytes)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error formatting base64 token value: %v", err))
return 1
}
c.Ui.Output(fmt.Sprintf("Root token: %s", token))
return 0
}
// initGenerateRoot is used to start the generation process
func (c *GenerateRootCommand) initGenerateRoot(client *api.Client, otp string, pgpKey string, drToken bool) int {
// Start the rekey
f := client.Sys().GenerateRootInit
if drToken {
f = client.Sys().GenerateDROperationTokenInit
}
status, err := f(otp, pgpKey)
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing root generation: %s", err))
return 1
}
c.dumpStatus(status)
return 0
}
// cancelGenerateRoot is used to abort the generation process
func (c *GenerateRootCommand) cancelGenerateRoot(client *api.Client, drToken bool) int {
f := client.Sys().GenerateRootCancel
if drToken {
f = client.Sys().GenerateDROperationTokenCancel
}
err := f()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to cancel root generation: %s", err))
return 1
}
c.Ui.Output("Root generation canceled.")
return 0
}
// rootGenerationStatus is used just to fetch and dump the status
func (c *GenerateRootCommand) rootGenerationStatus(client *api.Client, drToken bool) int {
// Check the status
f := client.Sys().GenerateRootStatus
if drToken {
f = client.Sys().GenerateDROperationTokenStatus
}
status, err := f()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error reading root generation status: %s", err))
return 1
}
c.dumpStatus(status)
return 0
}
// dumpStatus dumps the status to output
func (c *GenerateRootCommand) dumpStatus(status *api.GenerateRootStatusResponse) {
// Dump the status
statString := fmt.Sprintf(
"Nonce: %s\n"+
"Started: %v\n"+
"Generate Root Progress: %d\n"+
"Required Keys: %d\n"+
"Complete: %t",
status.Nonce,
status.Started,
status.Progress,
status.Required,
status.Complete,
)
if len(status.PGPFingerprint) > 0 {
statString = fmt.Sprintf("%s\nPGP Fingerprint: %s", statString, status.PGPFingerprint)
}
if len(status.EncodedRootToken) > 0 {
statString = fmt.Sprintf("%s\n\nEncoded root token: %s", statString, status.EncodedRootToken)
} else if len(status.EncodedToken) > 0 {
statString = fmt.Sprintf("%s\n\nEncoded token: %s", statString, status.EncodedToken)
}
c.Ui.Output(statString)
}
func (c *GenerateRootCommand) Synopsis() string {
return "Generates a new root token"
}
func (c *GenerateRootCommand) Help() string {
helpText := `
Usage: vault generate-root [options] [key]
'generate-root' is used to create a new root token.
Root generation can only be done when the vault is already unsealed. The
operation is done online, but requires that a threshold of the current unseal
keys be provided.
One (and only one) of the following must be provided when initializing the
root generation attempt:
1) A 16-byte, base64-encoded One Time Password (OTP) provided in the '-otp'
flag; the token is XOR'd with this value before it is returned once the final
unseal key has been provided. The '-decode' operation can be used with this
value and the OTP to output the final token value. The '-genotp' flag can be
used to generate a suitable value.
or
2) A file containing a PGP key (binary or base64-encoded) or a Keybase.io
username in the format of "keybase:<username>" in the '-pgp-key' flag. The
final token value will be encrypted with this public key and base64-encoded.
General Options:
` + meta.GeneralOptionsUsage() + `
Generate Root Options:
-init Initialize the root generation attempt. This can only
be done if no generation is already initiated.
-cancel Reset the root generation process by throwing away
prior unseal keys and the configuration.
-status Prints the status of the current attempt. This can be
used to see the status without attempting to provide
an unseal key.
-decode=abcd Decodes and outputs the generated root token. The OTP
used at '-init' time must be provided in the '-otp'
parameter.
-genotp Returns a high-quality OTP suitable for passing into
the '-init' method.
-otp=abcd The base64-encoded 16-byte OTP for use with the
'-init' or '-decode' methods.
-pgp-key A file on disk containing a binary- or base64-format
public PGP key, or a Keybase username specified as
"keybase:<username>". The output root token will be
encrypted and base64-encoded, in order, with the given
public key.
-nonce=abcd The nonce provided at initialization time. This same
nonce value must be provided with each unseal key. If
the unseal key is not being passed in via the command
line the nonce parameter is not required, and will
instead be displayed with the key prompt.
-dr-token Generate a Disaster Recovery operation token. This flag
should be set on '-init', '-cancel', and every time a
key is provided to specify the type of token to generate.
`
return strings.TrimSpace(helpText)
}
func (c *GenerateRootCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictNothing
}
func (c *GenerateRootCommand) AutocompleteFlags() complete.Flags {
return complete.Flags{
"-init": complete.PredictNothing,
"-cancel": complete.PredictNothing,
"-status": complete.PredictNothing,
"-decode": complete.PredictNothing,
"-genotp": complete.PredictNothing,
"-otp": complete.PredictNothing,
"-pgp-key": complete.PredictNothing,
"-nonce": complete.PredictNothing,
}
}

View file

@ -1,295 +0,0 @@
package command
import (
"context"
"encoding/base64"
"encoding/hex"
"os"
"strings"
"testing"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/helper/xor"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func TestGenerateRoot_Cancel(t *testing.T) {
core, _, _ := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &GenerateRootCommand{
Meta: meta.Meta{
Ui: ui,
},
}
otpBytes, err := vault.GenerateRandBytes(16)
if err != nil {
t.Fatal(err)
}
otp := base64.StdEncoding.EncodeToString(otpBytes)
args := []string{"-address", addr, "-init", "-otp", otp}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
args = []string{"-address", addr, "-cancel"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
config, err := core.GenerateRootConfiguration()
if err != nil {
t.Fatalf("err: %s", err)
}
if config != nil {
t.Fatal("should not have a config for root generation")
}
}
func TestGenerateRoot_status(t *testing.T) {
core, _, _ := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &GenerateRootCommand{
Meta: meta.Meta{
Ui: ui,
},
}
otpBytes, err := vault.GenerateRandBytes(16)
if err != nil {
t.Fatal(err)
}
otp := base64.StdEncoding.EncodeToString(otpBytes)
args := []string{"-address", addr, "-init", "-otp", otp}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
args = []string{"-address", addr, "-status"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
if !strings.Contains(ui.OutputWriter.String(), "Started: true") {
t.Fatalf("bad: %s", ui.OutputWriter.String())
}
}
func TestGenerateRoot_OTP(t *testing.T) {
core, ts, keys, _ := vault.TestCoreWithTokenStore(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &GenerateRootCommand{
Meta: meta.Meta{
Ui: ui,
},
}
// Generate an OTP
otpBytes, err := vault.GenerateRandBytes(16)
if err != nil {
t.Fatal(err)
}
otp := base64.StdEncoding.EncodeToString(otpBytes)
// Init the attempt
args := []string{
"-address", addr,
"-init",
"-otp", otp,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
config, err := core.GenerateRootConfiguration()
if err != nil {
t.Fatalf("err: %v", err)
}
for _, key := range keys {
ui = new(cli.MockUi)
c = &GenerateRootCommand{
Key: hex.EncodeToString(key),
Meta: meta.Meta{
Ui: ui,
},
}
c.Nonce = config.Nonce
// Provide the key
args = []string{
"-address", addr,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
beforeNAfter := strings.Split(ui.OutputWriter.String(), "Encoded root token: ")
if len(beforeNAfter) != 2 {
t.Fatalf("did not find encoded root token in %s", ui.OutputWriter.String())
}
encodedToken := strings.TrimSpace(beforeNAfter[1])
decodedToken, err := xor.XORBase64(encodedToken, otp)
if err != nil {
t.Fatal(err)
}
token, err := uuid.FormatUUID(decodedToken)
if err != nil {
t.Fatal(err)
}
req := logical.TestRequest(t, logical.ReadOperation, "lookup-self")
req.ClientToken = token
resp, err := ts.HandleRequest(context.Background(), req)
if err != nil {
t.Fatalf("error running token lookup-self: %v", err)
}
if resp == nil {
t.Fatalf("got nil resp with token lookup-self")
}
if resp.Data == nil {
t.Fatalf("got nil resp.Data with token lookup-self")
}
if resp.Data["orphan"].(bool) != true ||
resp.Data["ttl"].(int64) != 0 ||
resp.Data["num_uses"].(int) != 0 ||
resp.Data["meta"].(map[string]string) != nil ||
len(resp.Data["policies"].([]string)) != 1 ||
resp.Data["policies"].([]string)[0] != "root" {
t.Fatalf("bad: %#v", resp.Data)
}
// Clear the output and run a decode to verify we get the same result
ui.OutputWriter.Reset()
args = []string{
"-address", addr,
"-decode", encodedToken,
"-otp", otp,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
beforeNAfter = strings.Split(ui.OutputWriter.String(), "Root token: ")
if len(beforeNAfter) != 2 {
t.Fatalf("did not find decoded root token in %s", ui.OutputWriter.String())
}
outToken := strings.TrimSpace(beforeNAfter[1])
if outToken != token {
t.Fatalf("tokens do not match:\n%s\n%s", token, outToken)
}
}
func TestGenerateRoot_PGP(t *testing.T) {
core, ts, keys, _ := vault.TestCoreWithTokenStore(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &GenerateRootCommand{
Meta: meta.Meta{
Ui: ui,
},
}
tempDir, pubFiles, err := getPubKeyFiles(t)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
// Init the attempt
args := []string{
"-address", addr,
"-init",
"-pgp-key", pubFiles[0],
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
config, err := core.GenerateRootConfiguration()
if err != nil {
t.Fatalf("err: %v", err)
}
for _, key := range keys {
c = &GenerateRootCommand{
Key: hex.EncodeToString(key),
Meta: meta.Meta{
Ui: ui,
},
}
c.Nonce = config.Nonce
// Provide the key
args = []string{
"-address", addr,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}
beforeNAfter := strings.Split(ui.OutputWriter.String(), "Encoded root token: ")
if len(beforeNAfter) != 2 {
t.Fatalf("did not find encoded root token in %s", ui.OutputWriter.String())
}
encodedToken := strings.TrimSpace(beforeNAfter[1])
ptBuf, err := pgpkeys.DecryptBytes(encodedToken, pgpkeys.TestPrivKey1)
if err != nil {
t.Fatal(err)
}
if ptBuf == nil {
t.Fatal("returned plain text buffer is nil")
}
token := ptBuf.String()
req := logical.TestRequest(t, logical.ReadOperation, "lookup-self")
req.ClientToken = token
resp, err := ts.HandleRequest(context.Background(), req)
if err != nil {
t.Fatalf("error running token lookup-self: %v", err)
}
if resp == nil {
t.Fatalf("got nil resp with token lookup-self")
}
if resp.Data == nil {
t.Fatalf("got nil resp.Data with token lookup-self")
}
if resp.Data["orphan"].(bool) != true ||
resp.Data["ttl"].(int64) != 0 ||
resp.Data["num_uses"].(int) != 0 ||
resp.Data["meta"].(map[string]string) != nil ||
len(resp.Data["policies"].([]string)) != 1 ||
resp.Data["policies"].([]string)[0] != "root" {
t.Fatalf("bad: %#v", resp.Data)
}
}

View file

@ -1,406 +0,0 @@
package command
import (
"fmt"
"net/url"
"os"
"runtime"
"strings"
consulapi "github.com/hashicorp/consul/api"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/physical/consul"
"github.com/posener/complete"
)
// InitCommand is a Command that initializes a new Vault server.
type InitCommand struct {
meta.Meta
}
func (c *InitCommand) Run(args []string) int {
var threshold, shares, storedShares, recoveryThreshold, recoveryShares int
var pgpKeys, recoveryPgpKeys, rootTokenPgpKey pgpkeys.PubKeyFilesFlag
var auto, check bool
var consulServiceName string
flags := c.Meta.FlagSet("init", meta.FlagSetDefault)
flags.Usage = func() { c.Ui.Error(c.Help()) }
flags.IntVar(&shares, "key-shares", 5, "")
flags.IntVar(&threshold, "key-threshold", 3, "")
flags.IntVar(&storedShares, "stored-shares", 0, "")
flags.Var(&pgpKeys, "pgp-keys", "")
flags.Var(&rootTokenPgpKey, "root-token-pgp-key", "")
flags.IntVar(&recoveryShares, "recovery-shares", 5, "")
flags.IntVar(&recoveryThreshold, "recovery-threshold", 3, "")
flags.Var(&recoveryPgpKeys, "recovery-pgp-keys", "")
flags.BoolVar(&check, "check", false, "")
flags.BoolVar(&auto, "auto", false, "")
flags.StringVar(&consulServiceName, "consul-service", consul.DefaultServiceName, "")
if err := flags.Parse(args); err != nil {
return 1
}
initRequest := &api.InitRequest{
SecretShares: shares,
SecretThreshold: threshold,
StoredShares: storedShares,
PGPKeys: pgpKeys,
RecoveryShares: recoveryShares,
RecoveryThreshold: recoveryThreshold,
RecoveryPGPKeys: recoveryPgpKeys,
}
switch len(rootTokenPgpKey) {
case 0:
case 1:
initRequest.RootTokenPGPKey = rootTokenPgpKey[0]
default:
c.Ui.Error("Only one PGP key can be specified for encrypting the root token")
return 1
}
// If running in 'auto' mode, run service discovery based on environment
// variables of Consul.
if auto {
// Create configuration for Consul
consulConfig := consulapi.DefaultConfig()
// Create a client to communicate with Consul
consulClient, err := consulapi.NewClient(consulConfig)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to create Consul client:%v", err))
return 1
}
// Fetch Vault's protocol scheme from the client
vaultclient, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to fetch Vault client: %v", err))
return 1
}
if vaultclient.Address() == "" {
c.Ui.Error("Failed to fetch Vault client address")
return 1
}
clientURL, err := url.Parse(vaultclient.Address())
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to parse Vault address: %v", err))
return 1
}
if clientURL == nil {
c.Ui.Error("Failed to parse Vault client address")
return 1
}
var uninitializedVaults []string
var initializedVault string
// Query the nodes belonging to the cluster
if services, _, err := consulClient.Catalog().Service(consulServiceName, "", &consulapi.QueryOptions{AllowStale: true}); err == nil {
Loop:
for _, service := range services {
vaultAddress := &url.URL{
Scheme: clientURL.Scheme,
Host: fmt.Sprintf("%s:%d", service.ServiceAddress, service.ServicePort),
}
// Set VAULT_ADDR to the discovered node
os.Setenv(api.EnvVaultAddress, vaultAddress.String())
// Create a client to communicate with the discovered node
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf("Error initializing client: %v", err))
return 1
}
// Check the initialization status of the discovered node
inited, err := client.Sys().InitStatus()
switch {
case err != nil:
c.Ui.Error(fmt.Sprintf("Error checking initialization status of discovered node: %+q. Err: %v", vaultAddress.String(), err))
return 1
case inited:
// One of the nodes in the cluster is initialized. Break out.
initializedVault = vaultAddress.String()
break Loop
default:
// Vault is uninitialized.
uninitializedVaults = append(uninitializedVaults, vaultAddress.String())
}
}
}
export := "export"
quote := "'"
if runtime.GOOS == "windows" {
export = "set"
quote = ""
}
if initializedVault != "" {
vaultURL, err := url.Parse(initializedVault)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to parse Vault address: %+q. Err: %v", initializedVault, err))
}
c.Ui.Output(fmt.Sprintf("Discovered an initialized Vault node at %+q, using Consul service name %+q", vaultURL.String(), consulServiceName))
c.Ui.Output("\nSet the following environment variable to operate on the discovered Vault:\n")
c.Ui.Output(fmt.Sprintf("\t%s VAULT_ADDR=%s%s%s", export, quote, vaultURL.String(), quote))
return 0
}
switch len(uninitializedVaults) {
case 0:
c.Ui.Error(fmt.Sprintf("Failed to discover Vault nodes using Consul service name %+q", consulServiceName))
return 1
case 1:
// There was only one node found in the Vault cluster and it
// was uninitialized.
vaultURL, err := url.Parse(uninitializedVaults[0])
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to parse Vault address: %+q. Err: %v", uninitializedVaults[0], err))
}
// Set the VAULT_ADDR to the discovered node. This will ensure
// that the client created will operate on the discovered node.
os.Setenv(api.EnvVaultAddress, vaultURL.String())
// Let the client know that initialization is perfomed on the
// discovered node.
c.Ui.Output(fmt.Sprintf("Discovered Vault at %+q using Consul service name %+q\n", vaultURL.String(), consulServiceName))
// Attempt initializing it
ret := c.runInit(check, initRequest)
// Regardless of success or failure, instruct client to update VAULT_ADDR
c.Ui.Output("\nSet the following environment variable to operate on the discovered Vault:\n")
c.Ui.Output(fmt.Sprintf("\t%s VAULT_ADDR=%s%s%s", export, quote, vaultURL.String(), quote))
return ret
default:
// If more than one Vault node were discovered, print out all of them,
// requiring the client to update VAULT_ADDR and to run init again.
c.Ui.Output(fmt.Sprintf("Discovered more than one uninitialized Vaults using Consul service name %+q\n", consulServiceName))
c.Ui.Output("To initialize these Vaults, set any *one* of the following environment variables and run 'vault init':")
// Print valid commands to make setting the variables easier
for _, vaultNode := range uninitializedVaults {
vaultURL, err := url.Parse(vaultNode)
if err != nil {
c.Ui.Error(fmt.Sprintf("Failed to parse Vault address: %+q. Err: %v", vaultNode, err))
}
c.Ui.Output(fmt.Sprintf("\t%s VAULT_ADDR=%s%s%s", export, quote, vaultURL.String(), quote))
}
return 0
}
}
return c.runInit(check, initRequest)
}
func (c *InitCommand) runInit(check bool, initRequest *api.InitRequest) int {
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 1
}
if check {
return c.checkStatus(client)
}
resp, err := client.Sys().Init(initRequest)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing Vault: %s", err))
return 1
}
for i, key := range resp.Keys {
if resp.KeysB64 != nil && len(resp.KeysB64) == len(resp.Keys) {
c.Ui.Output(fmt.Sprintf("Unseal Key %d: %s", i+1, resp.KeysB64[i]))
} else {
c.Ui.Output(fmt.Sprintf("Unseal Key %d: %s", i+1, key))
}
}
for i, key := range resp.RecoveryKeys {
if resp.RecoveryKeysB64 != nil && len(resp.RecoveryKeysB64) == len(resp.RecoveryKeys) {
c.Ui.Output(fmt.Sprintf("Recovery Key %d: %s", i+1, resp.RecoveryKeysB64[i]))
} else {
c.Ui.Output(fmt.Sprintf("Recovery Key %d: %s", i+1, key))
}
}
c.Ui.Output(fmt.Sprintf("Initial Root Token: %s", resp.RootToken))
if initRequest.StoredShares < 1 {
c.Ui.Output(fmt.Sprintf(
"\n"+
"Vault initialized with %d keys and a key threshold of %d. Please\n"+
"securely distribute the above keys. When the vault is re-sealed,\n"+
"restarted, or stopped, you must provide at least %d of these keys\n"+
"to unseal it again.\n\n"+
"Vault does not store the master key. Without at least %d keys,\n"+
"your vault will remain permanently sealed.",
initRequest.SecretShares,
initRequest.SecretThreshold,
initRequest.SecretThreshold,
initRequest.SecretThreshold,
))
} else {
c.Ui.Output(
"\n" +
"Vault initialized successfully.",
)
}
if len(resp.RecoveryKeys) > 0 {
c.Ui.Output(fmt.Sprintf(
"\n"+
"Recovery key initialized with %d keys and a key threshold of %d. Please\n"+
"securely distribute the above keys.",
initRequest.RecoveryShares,
initRequest.RecoveryThreshold,
))
}
return 0
}
func (c *InitCommand) checkStatus(client *api.Client) int {
inited, err := client.Sys().InitStatus()
switch {
case err != nil:
c.Ui.Error(fmt.Sprintf(
"Error checking initialization status: %s", err))
return 1
case inited:
c.Ui.Output("Vault has been initialized")
return 0
default:
c.Ui.Output("Vault is not initialized")
return 2
}
}
func (c *InitCommand) Synopsis() string {
return "Initialize a new Vault server"
}
func (c *InitCommand) Help() string {
helpText := `
Usage: vault init [options]
Initialize a new Vault server.
This command connects to a Vault server and initializes it for the
first time. This sets up the initial set of master keys and the
backend data store structure.
This command can't be called on an already-initialized Vault server.
General Options:
` + meta.GeneralOptionsUsage() + `
Init Options:
-check Don't actually initialize, just check if Vault is
already initialized. A return code of 0 means Vault
is initialized; a return code of 2 means Vault is not
initialized; a return code of 1 means an error was
encountered.
-key-shares=5 The number of key shares to split the master key
into.
-key-threshold=3 The number of key shares required to reconstruct
the master key.
-stored-shares=0 The number of unseal keys to store. Only used with
Vault HSM. Must currently be equivalent to the
number of shares.
-pgp-keys If provided, must be a comma-separated list of
files on disk containing binary- or base64-format
public PGP keys, or Keybase usernames specified as
"keybase:<username>". The output unseal keys will
be encrypted and base64-encoded, in order, with the
given public keys. If you want to use them with the
'vault unseal' command, you will need to base64-
decode and decrypt; this will be the plaintext
unseal key. When 'stored-shares' are not used, the
number of entries in this field must match 'key-shares'.
When 'stored-shares' are used, the number of entries
should match the difference between 'key-shares'
and 'stored-shares'.
-root-token-pgp-key If provided, a file on disk with a binary- or
base64-format public PGP key, or a Keybase username
specified as "keybase:<username>". The output root
token will be encrypted and base64-encoded, in
order, with the given public key. You will need
to base64-decode and decrypt the result.
-recovery-shares=5 The number of key shares to split the recovery key
into. Only used with Vault HSM.
-recovery-threshold=3 The number of key shares required to reconstruct
the recovery key. Only used with Vault HSM.
-recovery-pgp-keys If provided, behaves like "pgp-keys" but for the
recovery key shares. Only used with Vault HSM.
-auto If set, performs service discovery using Consul.
When all the nodes of a Vault cluster are
registered with Consul, setting this flag will
trigger service discovery using the service name
with which Vault nodes are registered. This option
works well when each Vault cluster is registered
under a unique service name. Note that, when Consul
is serving as Vault's HA backend, Vault nodes are
registered with Consul by default. The service name
can be changed using 'consul-service' flag. Ensure
that environment variables required to communicate
with Consul, like (CONSUL_HTTP_ADDR,
CONSUL_HTTP_TOKEN, CONSUL_HTTP_SSL, et al) are
properly set. When only one Vault node is
discovered, it will be initialized and when more
than one Vault node is discovered, they will be
output for easy selection.
-consul-service Service name under which all the nodes of a Vault
cluster are registered with Consul. Note that, when
Vault uses Consul as its HA backend, by default,
Vault will register itself as a service with Consul
with the service name "vault". This name can be
modified in Vault's configuration file, using the
"service" option for the Consul backend.
`
return strings.TrimSpace(helpText)
}
func (c *InitCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictNothing
}
func (c *InitCommand) AutocompleteFlags() complete.Flags {
return complete.Flags{
"-check": complete.PredictNothing,
"-key-shares": complete.PredictNothing,
"-key-threshold": complete.PredictNothing,
"-pgp-keys": complete.PredictNothing,
"-root-token-pgp-key": complete.PredictNothing,
"-recovery-shares": complete.PredictNothing,
"-recovery-threshold": complete.PredictNothing,
"-recovery-pgp-keys": complete.PredictNothing,
"-auto": complete.PredictNothing,
"-consul-service": complete.PredictNothing,
}
}

View file

@ -1,343 +0,0 @@
package command
import (
"bytes"
"encoding/base64"
"os"
"reflect"
"regexp"
"strings"
"testing"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/keybase/go-crypto/openpgp"
"github.com/keybase/go-crypto/openpgp/packet"
"github.com/mitchellh/cli"
)
func TestInit(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: meta.Meta{
Ui: ui,
},
}
core := vault.TestCore(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
init, err := core.Initialized()
if err != nil {
t.Fatalf("err: %s", err)
}
if init {
t.Fatal("should not be initialized")
}
args := []string{"-address", addr}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
init, err = core.Initialized()
if err != nil {
t.Fatalf("err: %s", err)
}
if !init {
t.Fatal("should be initialized")
}
sealConf, err := core.SealAccess().BarrierConfig()
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &vault.SealConfig{
Type: "shamir",
SecretShares: 5,
SecretThreshold: 3,
}
if !reflect.DeepEqual(expected, sealConf) {
t.Fatalf("expected:\n%#v\ngot:\n%#v\n", expected, sealConf)
}
}
func TestInit_Check(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: meta.Meta{
Ui: ui,
},
}
core := vault.TestCore(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
// Should return 2, not initialized
args := []string{"-address", addr, "-check"}
if code := c.Run(args); code != 2 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Now initialize it
args = []string{"-address", addr}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Should return 0, initialized
args = []string{"-address", addr, "-check"}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
init, err := core.Initialized()
if err != nil {
t.Fatalf("err: %s", err)
}
if !init {
t.Fatal("should be initialized")
}
}
func TestInit_custom(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: meta.Meta{
Ui: ui,
},
}
core := vault.TestCore(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
init, err := core.Initialized()
if err != nil {
t.Fatalf("err: %s", err)
}
if init {
t.Fatal("should not be initialized")
}
args := []string{
"-address", addr,
"-key-shares", "7",
"-key-threshold", "3",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
init, err = core.Initialized()
if err != nil {
t.Fatalf("err: %s", err)
}
if !init {
t.Fatal("should be initialized")
}
sealConf, err := core.SealAccess().BarrierConfig()
if err != nil {
t.Fatalf("err: %s", err)
}
expected := &vault.SealConfig{
Type: "shamir",
SecretShares: 7,
SecretThreshold: 3,
}
if !reflect.DeepEqual(expected, sealConf) {
t.Fatalf("expected:\n%#v\ngot:\n%#v\n", expected, sealConf)
}
re, err := regexp.Compile("\\s+Initial Root Token:\\s+(.*)")
if err != nil {
t.Fatalf("Error compiling regex: %s", err)
}
matches := re.FindAllStringSubmatch(ui.OutputWriter.String(), -1)
if len(matches) != 1 {
t.Fatalf("Unexpected number of tokens found, got %d", len(matches))
}
rootToken := matches[0][1]
client, err := c.Client()
if err != nil {
t.Fatalf("Error fetching client: %v", err)
}
client.SetToken(rootToken)
re, err = regexp.Compile("\\s*Unseal Key \\d+: (.*)")
if err != nil {
t.Fatalf("Error compiling regex: %s", err)
}
matches = re.FindAllStringSubmatch(ui.OutputWriter.String(), -1)
if len(matches) != 7 {
t.Fatalf("Unexpected number of keys returned, got %d, matches was \n\n%#v\n\n, input was \n\n%s\n\n", len(matches), matches, ui.OutputWriter.String())
}
var unsealed bool
for i := 0; i < 3; i++ {
decodedKey, err := base64.StdEncoding.DecodeString(strings.TrimSpace(matches[i][1]))
if err != nil {
t.Fatalf("err decoding key %v: %v", matches[i][1], err)
}
unsealed, err = core.Unseal(decodedKey)
if err != nil {
t.Fatalf("err during unseal: %v; key was %v", err, matches[i][1])
}
}
if !unsealed {
t.Fatal("expected to be unsealed")
}
tokenInfo, err := client.Auth().Token().LookupSelf()
if err != nil {
t.Fatalf("Error looking up root token info: %v", err)
}
if tokenInfo.Data["policies"].([]interface{})[0].(string) != "root" {
t.Fatalf("expected root policy")
}
}
func TestInit_PGP(t *testing.T) {
ui := new(cli.MockUi)
c := &InitCommand{
Meta: meta.Meta{
Ui: ui,
},
}
core := vault.TestCore(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
init, err := core.Initialized()
if err != nil {
t.Fatalf("err: %s", err)
}
if init {
t.Fatal("should not be initialized")
}
tempDir, pubFiles, err := getPubKeyFiles(t)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
args := []string{
"-address", addr,
"-key-shares", "2",
"-pgp-keys", pubFiles[0] + ",@" + pubFiles[1] + "," + pubFiles[2],
"-key-threshold", "2",
"-root-token-pgp-key", pubFiles[0],
}
// This should fail, as key-shares does not match pgp-keys size
if code := c.Run(args); code == 0 {
t.Fatalf("bad (command should have failed): %d\n\n%s", code, ui.ErrorWriter.String())
}
args = []string{
"-address", addr,
"-key-shares", "4",
"-pgp-keys", pubFiles[0] + ",@" + pubFiles[1] + "," + pubFiles[2] + "," + pubFiles[3],
"-key-threshold", "2",
"-root-token-pgp-key", pubFiles[0],
}
ui.OutputWriter.Reset()
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
init, err = core.Initialized()
if err != nil {
t.Fatalf("err: %s", err)
}
if !init {
t.Fatal("should be initialized")
}
sealConf, err := core.SealAccess().BarrierConfig()
if err != nil {
t.Fatalf("err: %s", err)
}
pgpKeys := []string{}
for _, pubFile := range pubFiles {
pub, err := pgpkeys.ReadPGPFile(pubFile)
if err != nil {
t.Fatalf("bad: %v", err)
}
pgpKeys = append(pgpKeys, pub)
}
expected := &vault.SealConfig{
Type: "shamir",
SecretShares: 4,
SecretThreshold: 2,
PGPKeys: pgpKeys,
}
if !reflect.DeepEqual(expected, sealConf) {
t.Fatalf("expected:\n%#v\ngot:\n%#v\n", expected, sealConf)
}
re, err := regexp.Compile("\\s+Initial Root Token:\\s+(.*)")
if err != nil {
t.Fatalf("Error compiling regex: %s", err)
}
matches := re.FindAllStringSubmatch(ui.OutputWriter.String(), -1)
if len(matches) != 1 {
t.Fatalf("Unexpected number of tokens found, got %d", len(matches))
}
encRootToken := matches[0][1]
privKeyBytes, err := base64.StdEncoding.DecodeString(pgpkeys.TestPrivKey1)
if err != nil {
t.Fatalf("error decoding private key: %v", err)
}
ptBuf := bytes.NewBuffer(nil)
entity, err := openpgp.ReadEntity(packet.NewReader(bytes.NewBuffer(privKeyBytes)))
if err != nil {
t.Fatalf("Error parsing private key: %s", err)
}
var rootBytes []byte
rootBytes, err = base64.StdEncoding.DecodeString(encRootToken)
if err != nil {
t.Fatalf("Error decoding root token: %s", err)
}
entityList := &openpgp.EntityList{entity}
md, err := openpgp.ReadMessage(bytes.NewBuffer(rootBytes), entityList, nil, nil)
if err != nil {
t.Fatalf("Error decrypting root token: %s", err)
}
ptBuf.ReadFrom(md.UnverifiedBody)
rootToken := ptBuf.String()
parseDecryptAndTestUnsealKeys(t, ui.OutputWriter.String(), rootToken, false, nil, nil, core)
client, err := c.Client()
if err != nil {
t.Fatalf("Error fetching client: %v", err)
}
client.SetToken(rootToken)
tokenInfo, err := client.Auth().Token().LookupSelf()
if err != nil {
t.Fatalf("Error looking up root token info: %v", err)
}
if tokenInfo.Data["policies"].([]interface{})[0].(string) != "root" {
t.Fatalf("expected root policy")
}
}

View file

@ -1,55 +0,0 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/vault/meta"
)
// KeyStatusCommand is a Command that provides information about the key status
type KeyStatusCommand struct {
meta.Meta
}
func (c *KeyStatusCommand) Run(args []string) int {
flags := c.Meta.FlagSet("key-status", meta.FlagSetDefault)
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 2
}
status, err := client.Sys().KeyStatus()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error reading audits: %s", err))
return 2
}
c.Ui.Output(fmt.Sprintf("Key Term: %d", status.Term))
c.Ui.Output(fmt.Sprintf("Installation Time: %v", status.InstallTime))
return 0
}
func (c *KeyStatusCommand) Synopsis() string {
return "Provides information about the active encryption key"
}
func (c *KeyStatusCommand) Help() string {
helpText := `
Usage: vault key-status [options]
Provides information about the active encryption key. Specifically,
the current key term and the key installation time.
General Options:
` + meta.GeneralOptionsUsage()
return strings.TrimSpace(helpText)
}

View file

@ -1,31 +0,0 @@
package command
import (
"testing"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func TestKeyStatus(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &KeyStatusCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
},
}
args := []string{
"-address", addr,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}

40
command/lease.go Normal file
View file

@ -0,0 +1,40 @@
package command
import (
"strings"
"github.com/mitchellh/cli"
)
var _ cli.Command = (*LeaseCommand)(nil)
type LeaseCommand struct {
*BaseCommand
}
func (c *LeaseCommand) Synopsis() string {
return "Interact with leases"
}
func (c *LeaseCommand) Help() string {
helpText := `
Usage: vault lease <subcommand> [options] [args]
This command groups subcommands for interacting with leases. Users can revoke
or renew leases.
Renew a lease:
$ vault lease renew database/creds/readonly/2f6a614c...
Revoke a lease:
$ vault lease revoke database/creds/readonly/2f6a614c...
`
return strings.TrimSpace(helpText)
}
func (c *LeaseCommand) Run(args []string) int {
return cli.RunResultHelp
}

127
command/lease_renew.go Normal file
View file

@ -0,0 +1,127 @@
package command
import (
"fmt"
"strings"
"time"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*LeaseRenewCommand)(nil)
var _ cli.CommandAutocomplete = (*LeaseRenewCommand)(nil)
type LeaseRenewCommand struct {
*BaseCommand
flagIncrement time.Duration
}
func (c *LeaseRenewCommand) Synopsis() string {
return "Renews the lease of a secret"
}
func (c *LeaseRenewCommand) Help() string {
helpText := `
Usage: vault lease renew [options] ID
Renews the lease on a secret, extending the time that it can be used before
it is revoked by Vault.
Every secret in Vault has a lease associated with it. If the owner of the
secret wants to use it longer than the lease, then it must be renewed.
Renewing the lease does not change the contents of the secret. The ID is the
full path lease ID.
Renew a secret:
$ vault lease renew database/creds/readonly/2f6a614c...
Lease renewal will fail if the secret is not renewable, the secret has already
been revoked, or if the secret has already reached its maximum TTL.
For a full list of examples, please see the documentation.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *LeaseRenewCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
f := set.NewFlagSet("Command Options")
f.DurationVar(&DurationVar{
Name: "increment",
Target: &c.flagIncrement,
Default: 0,
EnvVar: "",
Completion: complete.PredictAnything,
Usage: "Request a specific increment in seconds. Vault is not required " +
"to honor this request.",
})
return set
}
func (c *LeaseRenewCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictAnything
}
func (c *LeaseRenewCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *LeaseRenewCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
leaseID := ""
increment := c.flagIncrement
args = f.Args()
switch len(args) {
case 0:
c.UI.Error("Missing ID!")
return 1
case 1:
leaseID = strings.TrimSpace(args[0])
case 2:
// Deprecation
// TODO: remove in 0.9.0
c.UI.Warn(wrapAtLength(
"WARNING! Specifying INCREMENT as a second argument is deprecated. " +
"Please use -increment instead. This will be removed in the next " +
"major release of Vault."))
leaseID = strings.TrimSpace(args[0])
parsed, err := time.ParseDuration(appendDurationSuffix(args[1]))
if err != nil {
c.UI.Error(fmt.Sprintf("Invalid increment: %s", err))
return 1
}
increment = parsed
default:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1-2, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
secret, err := client.Sys().Renew(leaseID, truncateToSeconds(increment))
if err != nil {
c.UI.Error(fmt.Sprintf("Error renewing %s: %s", leaseID, err))
return 2
}
return OutputSecret(c.UI, c.flagFormat, secret)
}

170
command/lease_renew_test.go Normal file
View file

@ -0,0 +1,170 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
)
func testLeaseRenewCommand(tb testing.TB) (*cli.MockUi, *LeaseRenewCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &LeaseRenewCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
// testLeaseRenewCommandMountAndLease mounts a leased secret backend and returns
// the leaseID of an item.
func testLeaseRenewCommandMountAndLease(tb testing.TB, client *api.Client) string {
if err := client.Sys().Mount("testing", &api.MountInput{
Type: "generic-leased",
}); err != nil {
tb.Fatal(err)
}
if _, err := client.Logical().Write("testing/foo", map[string]interface{}{
"key": "value",
"lease": "5m",
}); err != nil {
tb.Fatal(err)
}
// Read the secret back to get the leaseID
secret, err := client.Logical().Read("testing/foo")
if err != nil {
tb.Fatal(err)
}
if secret == nil || secret.LeaseID == "" {
tb.Fatalf("missing secret or lease: %#v", secret)
}
return secret.LeaseID
}
func TestLeaseRenewCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"empty",
nil,
"Missing ID!",
1,
},
{
"increment",
[]string{"-increment", "60s"},
"foo",
0,
},
{
"increment_no_suffix",
[]string{"-increment", "60"},
"foo",
0,
},
{
"format",
[]string{"-format", "json"},
"{",
0,
},
{
"format_bad",
[]string{"-format", "nope-not-real"},
"Invalid output format",
1,
},
}
t.Run("group", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
leaseID := testLeaseRenewCommandMountAndLease(t, client)
ui, cmd := testLeaseRenewCommand(t)
cmd.client = client
if tc.args != nil {
tc.args = append(tc.args, leaseID)
}
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("integration", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
leaseID := testLeaseRenewCommandMountAndLease(t, client)
_, cmd := testLeaseRenewCommand(t)
cmd.client = client
code := cmd.Run([]string{leaseID})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testLeaseRenewCommand(t)
cmd.client = client
code := cmd.Run([]string{
"foo/bar",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error renewing foo/bar: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testLeaseRenewCommand(t)
assertNoTabs(t, cmd)
})
}

142
command/lease_revoke.go Normal file
View file

@ -0,0 +1,142 @@
package command
import (
"fmt"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*LeaseRevokeCommand)(nil)
var _ cli.CommandAutocomplete = (*LeaseRevokeCommand)(nil)
type LeaseRevokeCommand struct {
*BaseCommand
flagForce bool
flagPrefix bool
}
func (c *LeaseRevokeCommand) Synopsis() string {
return "Revokes leases and secrets"
}
func (c *LeaseRevokeCommand) Help() string {
helpText := `
Usage: vault lease revoke [options] ID
Revokes secrets by their lease ID. This command can revoke a single secret
or multiple secrets based on a path-matched prefix.
Revoke a single lease:
$ vault lease revoke database/creds/readonly/2f6a614c...
Revoke all leases for a role:
$ vault lease revoke -prefix aws/creds/deploy
Force delete leases from Vault even if secret engine revocation fails:
$ vault lease revoke -force -prefix consul/creds
For a full list of examples and paths, please see the documentation that
corresponds to the secret engine in use.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *LeaseRevokeCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP)
f := set.NewFlagSet("Command Options")
f.BoolVar(&BoolVar{
Name: "force",
Aliases: []string{"f"},
Target: &c.flagForce,
Default: false,
Usage: "Delete the lease from Vault even if the secret engine revocation " +
"fails. This is meant for recovery situations where the secret " +
"in the target secret engine was manually removed. If this flag is " +
"specified, -prefix is also required.",
})
f.BoolVar(&BoolVar{
Name: "prefix",
Target: &c.flagPrefix,
Default: false,
Usage: "Treat the ID as a prefix instead of an exact lease ID. This can " +
"revoke multiple leases simultaneously.",
})
return set
}
func (c *LeaseRevokeCommand) AutocompleteArgs() complete.Predictor {
return c.PredictVaultFiles()
}
func (c *LeaseRevokeCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *LeaseRevokeCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
if c.flagForce && !c.flagPrefix {
c.UI.Error("Specifying -force requires also specifying -prefix")
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
leaseID := strings.TrimSpace(args[0])
switch {
case c.flagForce && c.flagPrefix:
c.UI.Warn(wrapAtLength("Warning! Force-removing leases can cause Vault " +
"to become out of sync with secret engines!"))
if err := client.Sys().RevokeForce(leaseID); err != nil {
c.UI.Error(fmt.Sprintf("Error force revoking leases with prefix %s: %s", leaseID, err))
return 2
}
c.UI.Output(fmt.Sprintf("Success! Force revoked any leases with prefix: %s", leaseID))
return 0
case c.flagPrefix:
if err := client.Sys().RevokePrefix(leaseID); err != nil {
c.UI.Error(fmt.Sprintf("Error revoking leases with prefix %s: %s", leaseID, err))
return 2
}
c.UI.Output(fmt.Sprintf("Success! Revoked any leases with prefix: %s", leaseID))
return 0
default:
if err := client.Sys().Revoke(leaseID); err != nil {
c.UI.Error(fmt.Sprintf("Error revoking lease %s: %s", leaseID, err))
return 2
}
c.UI.Output(fmt.Sprintf("Success! Revoked lease: %s", leaseID))
return 0
}
}

View file

@ -0,0 +1,134 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
)
func testLeaseRevokeCommand(tb testing.TB) (*cli.MockUi, *LeaseRevokeCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &LeaseRevokeCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestLeaseRevokeCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"force_without_prefix",
[]string{"-force"},
"requires also specifying -prefix",
1,
},
{
"single",
nil,
"Success",
0,
},
{
"force_prefix",
[]string{"-force", "-prefix"},
"Success",
0,
},
{
"prefix",
[]string{"-prefix"},
"Success",
0,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
if err := client.Sys().Mount("secret-leased", &api.MountInput{
Type: "generic-leased",
}); err != nil {
t.Fatal(err)
}
path := "secret-leased/revoke/" + tc.name
data := map[string]interface{}{
"key": "value",
"lease": "1m",
}
if _, err := client.Logical().Write(path, data); err != nil {
t.Fatal(err)
}
secret, err := client.Logical().Read(path)
if err != nil {
t.Fatal(err)
}
ui, cmd := testLeaseRevokeCommand(t)
cmd.client = client
tc.args = append(tc.args, secret.LeaseID)
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testLeaseRevokeCommand(t)
cmd.client = client
code := cmd.Run([]string{
"foo/bar",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error revoking lease foo/bar: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testLeaseRevokeCommand(t)
assertNoTabs(t, cmd)
})
}

View file

@ -1,97 +1,101 @@
package command
import (
"flag"
"fmt"
"strings"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/meta"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
// ListCommand is a Command that lists data from the Vault.
var _ cli.Command = (*ListCommand)(nil)
var _ cli.CommandAutocomplete = (*ListCommand)(nil)
type ListCommand struct {
meta.Meta
*BaseCommand
}
func (c *ListCommand) Synopsis() string {
return "List data or secrets"
}
func (c *ListCommand) Help() string {
helpText := `
Usage: vault list [options] PATH
Lists data from Vault at the given path. This can be used to list keys in a,
given secret engine.
List values under the "my-app" folder of the generic secret engine:
$ vault list secret/my-app/
For a full list of examples and paths, please see the documentation that
corresponds to the secret engine in use. Not all engines support listing.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *ListCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
}
func (c *ListCommand) AutocompleteArgs() complete.Predictor {
return c.PredictVaultFolders()
}
func (c *ListCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *ListCommand) Run(args []string) int {
var format string
var err error
var secret *api.Secret
var flags *flag.FlagSet
flags = c.Meta.FlagSet("list", meta.FlagSetDefault)
flags.StringVar(&format, "format", "table", "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = flags.Args()
if len(args) != 1 || len(args[0]) == 0 {
c.Ui.Error("list expects one argument")
flags.Usage()
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
path := args[0]
if path[0] == '/' {
path = path[1:]
}
if !strings.HasSuffix(path, "/") {
path = path + "/"
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
c.UI.Error(err.Error())
return 2
}
secret, err = client.Logical().List(path)
path := ensureTrailingSlash(sanitizePath(args[0]))
secret, err := client.Logical().List(path)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error reading %s: %s", path, err))
return 1
c.UI.Error(fmt.Sprintf("Error listing %s: %s", path, err))
return 2
}
if secret == nil {
c.Ui.Error(fmt.Sprintf(
"No value found at %s", path))
return 1
if secret == nil || secret.Data == nil {
c.UI.Error(fmt.Sprintf("No value found at %s", path))
return 2
}
// If the secret is wrapped, return the wrapped response.
if secret.WrapInfo != nil && secret.WrapInfo.TTL != 0 {
return OutputSecret(c.Ui, format, secret)
return OutputSecret(c.UI, c.flagFormat, secret)
}
if secret.Data["keys"] == nil {
c.Ui.Error("No entries found")
return 0
if _, ok := extractListData(secret); !ok {
c.UI.Error(fmt.Sprintf("No entries found at %s", path))
return 2
}
return OutputList(c.Ui, format, secret)
}
func (c *ListCommand) Synopsis() string {
return "List data or secrets in Vault"
}
func (c *ListCommand) Help() string {
helpText :=
`
Usage: vault list [options] path
List data from Vault.
Retrieve a listing of available data. The data returned, if any, is backend-
and endpoint-specific.
General Options:
` + meta.GeneralOptionsUsage() + `
Read Options:
-format=table The format for output. By default it is a whitespace-
delimited table. This can also be json or yaml.
`
return strings.TrimSpace(helpText)
return OutputList(c.UI, c.flagFormat, secret)
}

View file

@ -1,71 +1,150 @@
package command
import (
"reflect"
"strings"
"testing"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func TestList(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
func testListCommand(tb testing.TB) (*cli.MockUi, *ListCommand) {
tb.Helper()
ui := new(cli.MockUi)
c := &ReadCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
ui := cli.NewMockUi()
return ui, &ListCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestListCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"not_enough_args",
[]string{},
"Not enough arguments",
1,
},
{
"too_many_args",
[]string{"foo", "bar"},
"Too many arguments",
1,
},
{
"not_found",
[]string{"nope/not/once/never"},
"",
2,
},
{
"default",
[]string{"secret/list"},
"bar\nbaz\nfoo",
0,
},
{
"default_slash",
[]string{"secret/list/"},
"bar\nbaz\nfoo",
0,
},
{
"format",
[]string{
"-format", "json",
"secret/list/",
},
"[",
0,
},
{
"format_bad",
[]string{
"-format", "nope-not-real",
"secret/list/",
},
"Invalid output format",
1,
},
}
args := []string{
"-address", addr,
"-format", "json",
"secret",
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
// Run once so the client is setup, ignore errors
c.Run(args)
for _, tc := range cases {
tc := tc
// Get the client so we can write data
client, err := c.Client()
if err != nil {
t.Fatalf("err: %s", err)
}
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
data := map[string]interface{}{"value": "bar"}
if _, err := client.Logical().Write("secret/foo", data); err != nil {
t.Fatalf("err: %s", err)
}
client, closer := testVaultServer(t)
defer closer()
data = map[string]interface{}{"value": "bar"}
if _, err := client.Logical().Write("secret/foo/bar", data); err != nil {
t.Fatalf("err: %s", err)
}
keys := []string{
"secret/list/foo",
"secret/list/bar",
"secret/list/baz",
}
for _, k := range keys {
if _, err := client.Logical().Write(k, map[string]interface{}{
"foo": "bar",
}); err != nil {
t.Fatal(err)
}
}
secret, err := client.Logical().List("secret/")
if err != nil {
t.Fatalf("err: %s", err)
}
ui, cmd := testListCommand(t)
cmd.client = client
if secret == nil {
t.Fatalf("err: No value found at secret/")
}
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
if secret.Data == nil {
t.Fatalf("err: Data not found")
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
exp := map[string]interface{}{
"keys": []interface{}{"foo", "foo/"},
}
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
if !reflect.DeepEqual(secret.Data, exp) {
t.Fatalf("err: expected %#v, got %#v", exp, secret.Data)
}
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testListCommand(t)
cmd.client = client
code := cmd.Run([]string{
"secret/list",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error listing secret/list/: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testListCommand(t)
assertNoTabs(t, cmd)
})
}

379
command/login.go Normal file
View file

@ -0,0 +1,379 @@
package command
import (
"fmt"
"io"
"os"
"strings"
"github.com/hashicorp/vault/api"
"github.com/posener/complete"
)
// LoginHandler is the interface that any auth handlers must implement to enable
// auth via the CLI.
type LoginHandler interface {
Auth(*api.Client, map[string]string) (*api.Secret, error)
Help() string
}
type LoginCommand struct {
*BaseCommand
Handlers map[string]LoginHandler
flagMethod string
flagPath string
flagNoStore bool
flagTokenOnly bool
// Deprecations
// TODO: remove in 0.9.0
flagNoVerify bool
testStdin io.Reader // for tests
}
func (c *LoginCommand) Synopsis() string {
return "Authenticate locally"
}
func (c *LoginCommand) Help() string {
helpText := `
Usage: vault login [options] [AUTH K=V...]
Authenticates users or machines to Vault using the provided arguments. A
successful authentication results in a Vault token - conceptually similar to
a session token on a website. By default, this token is cached on the local
machine for future requests.
The default auth method is "token". If not supplied via the CLI,
Vault will prompt for input. If the argument is "-", the values are read
from stdin.
The -method flag allows using other auth methods, such as userpass, github, or
cert. For these, additional "K=V" pairs may be required. For example, to
authenticate to the userpass auth method:
$ vault login -method=userpass username=my-username
For more information about the list of configuration parameters available for
a given auth method, use the "vault auth help TYPE". You can also use "vault
auth list" to see the list of enabled auth methods.
If an auth method is enabled at a non-standard path, the -method flag still
refers to the canonical type, but the -path flag refers to the enabled path.
If a github auth method was enabled at "github-ent", authenticate like this:
$ vault login -method=github -path=github-prod
If the authentication is requested with response wrapping (via -wrap-ttl),
the returned token is automatically unwrapped unless:
- The -token-only flag is used, in which case this command will output
the wrapping token.
- The -no-store flag is used, in which case this command will output the
details of the wrapping token.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *LoginCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputField | FlagSetOutputFormat)
f := set.NewFlagSet("Command Options")
f.StringVar(&StringVar{
Name: "method",
Target: &c.flagMethod,
Default: "token",
Completion: c.PredictVaultAvailableAuths(),
Usage: "Type of authentication to use such as \"userpass\" or " +
"\"ldap\". Note this corresponds to the TYPE, not the enabled path. " +
"Use -path to specify the path where the authentication is enabled.",
})
f.StringVar(&StringVar{
Name: "path",
Target: &c.flagPath,
Default: "",
Completion: c.PredictVaultAuths(),
Usage: "Remote path in Vault where the auth method is enabled. " +
"This defaults to the TYPE of method (e.g. userpass -> userpass/).",
})
f.BoolVar(&BoolVar{
Name: "no-store",
Target: &c.flagNoStore,
Default: false,
Usage: "Do not persist the token to the token helper (usually the " +
"local filesystem) after authentication for use in future requests. " +
"The token will only be displayed in the command output.",
})
f.BoolVar(&BoolVar{
Name: "token-only",
Target: &c.flagTokenOnly,
Default: false,
Usage: "Output only the token with no verification. This flag is a " +
"shortcut for \"-field=token -no-store\". Setting those flags to other " +
"values will have no affect.",
})
// Deprecations
// TODO: remove in 0.9.0
f.BoolVar(&BoolVar{
Name: "no-verify",
Target: &c.flagNoVerify,
Hidden: true,
Default: false,
Usage: "",
})
return set
}
func (c *LoginCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *LoginCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *LoginCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
// Deprecations
// TODO: remove in 0.10.0
switch {
case c.flagNoVerify:
c.UI.Warn(wrapAtLength(
"WARNING! The -no-verify flag is deprecated. In the past, Vault " +
"performed a lookup on a token after authentication. This is no " +
"longer the case for all auth methods except \"token\". Vault will " +
"still attempt to perform a lookup when given a token directly " +
"because that is how it gets the list of policies, ttl, and other " +
"metadata. To disable this lookup, specify \"lookup=false\" as a " +
"configuration option to the token auth method, like this:"))
c.UI.Warn("")
c.UI.Warn(" $ vault auth token=ABCD lookup=false")
c.UI.Warn("")
c.UI.Warn("Or omit the token and Vault will prompt for it:")
c.UI.Warn("")
c.UI.Warn(" $ vault auth lookup=false")
c.UI.Warn(" Token (will be hidden): ...")
c.UI.Warn("")
c.UI.Warn(wrapAtLength(
"If you are not using token authentication, you can safely omit this " +
"flag. Vault will not perform a lookup after authentication."))
c.UI.Warn("")
// There's no point in passing this to other auth handlers...
if c.flagMethod == "token" {
args = append(args, "lookup=false")
}
}
// Set the right flags if the user requested token-only - this overrides
// any previously configured values, as documented.
if c.flagTokenOnly {
c.flagNoStore = true
c.flagField = "token"
}
// Get the auth method
authMethod := sanitizePath(c.flagMethod)
if authMethod == "" {
authMethod = "token"
}
// If no path is specified, we default the path to the method type
// or use the plugin name if it's a plugin
authPath := c.flagPath
if authPath == "" {
authPath = ensureTrailingSlash(authMethod)
}
// Get the handler function
authHandler, ok := c.Handlers[authMethod]
if !ok {
c.UI.Error(wrapAtLength(fmt.Sprintf(
"Unknown auth method: %s. Use \"vault auth list\" to see the "+
"complete list of auth methods. Additionally, some "+
"auth methods are only available via the HTTP API.",
authMethod)))
return 1
}
// Pull our fake stdin if needed
stdin := (io.Reader)(os.Stdin)
if c.testStdin != nil {
stdin = c.testStdin
}
// If the user provided a token, pass it along to the auth provier.
if authMethod == "token" && len(args) > 0 && !strings.Contains(args[0], "=") {
args = append([]string{"token=" + args[0]}, args[1:]...)
}
config, err := parseArgsDataString(stdin, args)
if err != nil {
c.UI.Error(fmt.Sprintf("Error parsing configuration: %s", err))
return 1
}
// If the user did not specify a mount path, use the provided mount path.
if config["mount"] == "" && authPath != "" {
config["mount"] = authPath
}
// Create the client
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
// Authenticate delegation to the auth handler
secret, err := authHandler.Auth(client, config)
if err != nil {
c.UI.Error(fmt.Sprintf("Error authenticating: %s", err))
return 2
}
// Unset any previous token wrapping functionality. If the original request
// was for a wrapped token, we don't want future requests to be wrapped.
client.SetWrappingLookupFunc(func(string, string) string { return "" })
// Recursively extract the token, handling wrapping
unwrap := !c.flagTokenOnly && !c.flagNoStore
secret, isWrapped, err := c.extractToken(client, secret, unwrap)
if err != nil {
c.UI.Error(fmt.Sprintf("Error extracting token: %s", err))
return 2
}
if secret == nil {
c.UI.Error("Vault returned an empty secret")
return 2
}
// Handle special cases if the token was wrapped
if isWrapped {
if c.flagTokenOnly {
return PrintRawField(c.UI, secret, "wrapping_token")
}
if c.flagNoStore {
return OutputSecret(c.UI, c.flagFormat, secret)
}
}
// If we got this far, verify we have authentication data before continuing
if secret.Auth == nil {
c.UI.Error(wrapAtLength(
"Vault returned a secret, but the secret has no authentication " +
"information attached. This should never happen and is likely a " +
"bug."))
return 2
}
// Pull the token itself out, since we don't need the rest of the auth
// information anymore/.
token := secret.Auth.ClientToken
if !c.flagNoStore {
// Grab the token helper so we can store
tokenHelper, err := c.TokenHelper()
if err != nil {
c.UI.Error(wrapAtLength(fmt.Sprintf(
"Error initializing token helper. Please verify that the token "+
"helper is available and properly configured for your system. The "+
"error was: %s", err)))
return 1
}
// Store the token in the local client
if err := tokenHelper.Store(token); err != nil {
c.UI.Error(fmt.Sprintf("Error storing token: %s", err))
c.UI.Error(wrapAtLength(
"Authentication was successful, but the token was not persisted. The "+
"resulting token is shown below for your records.") + "\n")
OutputSecret(c.UI, c.flagFormat, secret)
return 2
}
// Warn if the VAULT_TOKEN environment variable is set, as that will take
// precedence. We output as a warning, so piping should still work since it
// will be on a different stream.
if os.Getenv("VAULT_TOKEN") != "" {
c.UI.Warn(wrapAtLength("WARNING! The VAULT_TOKEN environment variable "+
"is set! This takes precedence over the value set by this command. To "+
"use the value set by this command, unset the VAULT_TOKEN environment "+
"variable or set it to the token displayed below.") + "\n")
}
} else {
c.UI.Warn(wrapAtLength(
"The token was not stored in token helper. Set the VAULT_TOKEN "+
"environment variable or pass the token below with each request to "+
"Vault.") + "\n")
}
// If the user requested a particular field, print that out now since we
// are likely piping to another process.
if c.flagField != "" {
return PrintRawField(c.UI, secret, c.flagField)
}
// Print some yay! text, but only in table mode.
if c.flagFormat == "table" {
c.UI.Output(wrapAtLength(
"Success! You are now authenticated. The token information displayed "+
"below is already stored in the token helper. You do NOT need to run "+
"\"vault login\" again. Future Vault requests will automatically use "+
"this token.") + "\n")
}
return OutputSecret(c.UI, c.flagFormat, secret)
}
// extractToken extracts the token from the given secret, automatically
// unwrapping responses and handling error conditions if unwrap is true. The
// result also returns whether it was a wrapped resonse that was not unwrapped.
func (c *LoginCommand) extractToken(client *api.Client, secret *api.Secret, unwrap bool) (*api.Secret, bool, error) {
switch {
case secret == nil:
return nil, false, fmt.Errorf("empty response from auth helper")
case secret.Auth != nil:
return secret, false, nil
case secret.WrapInfo != nil:
if secret.WrapInfo.WrappedAccessor == "" {
return nil, false, fmt.Errorf("wrapped response does not contain a token")
}
if !unwrap {
return secret, true, nil
}
client.SetToken(secret.WrapInfo.Token)
secret, err := client.Logical().Unwrap("")
if err != nil {
return nil, false, err
}
return c.extractToken(client, secret, unwrap)
default:
return nil, false, fmt.Errorf("no auth or wrapping info in response")
}
}

496
command/login_test.go Normal file
View file

@ -0,0 +1,496 @@
package command
import (
"strings"
"testing"
"github.com/mitchellh/cli"
"github.com/hashicorp/vault/api"
credToken "github.com/hashicorp/vault/builtin/credential/token"
credUserpass "github.com/hashicorp/vault/builtin/credential/userpass"
"github.com/hashicorp/vault/command/token"
)
func testLoginCommand(tb testing.TB) (*cli.MockUi, *LoginCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &LoginCommand{
BaseCommand: &BaseCommand{
UI: ui,
// Override to our own token helper
tokenHelper: token.NewTestingTokenHelper(),
},
Handlers: map[string]LoginHandler{
"token": &credToken.CLIHandler{},
"userpass": &credUserpass.CLIHandler{},
},
}
}
func TestLoginCommand_Run(t *testing.T) {
t.Parallel()
t.Run("custom_path", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
if err := client.Sys().EnableAuth("my-auth", "userpass", ""); err != nil {
t.Fatal(err)
}
if _, err := client.Logical().Write("auth/my-auth/users/test", map[string]interface{}{
"password": "test",
"policies": "default",
}); err != nil {
t.Fatal(err)
}
ui, cmd := testLoginCommand(t)
cmd.client = client
tokenHelper, err := cmd.TokenHelper()
if err != nil {
t.Fatal(err)
}
code := cmd.Run([]string{
"-method", "userpass",
"-path", "my-auth",
"username=test",
"password=test",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Success! You are now authenticated."
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to be %q", combined, expected)
}
storedToken, err := tokenHelper.Get()
if err != nil {
t.Fatal(err)
}
if l, exp := len(storedToken), 36; l != exp {
t.Errorf("expected token to be %d characters, was %d: %q", exp, l, storedToken)
}
})
t.Run("no_store", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{
Policies: []string{"default"},
TTL: "30m",
})
if err != nil {
t.Fatal(err)
}
token := secret.Auth.ClientToken
_, cmd := testLoginCommand(t)
cmd.client = client
tokenHelper, err := cmd.TokenHelper()
if err != nil {
t.Fatal(err)
}
// Ensure we have no token to start
if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" {
t.Errorf("expected token helper to be empty: %s: %q", err, storedToken)
}
code := cmd.Run([]string{
"-no-store",
token,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
storedToken, err := tokenHelper.Get()
if err != nil {
t.Fatal(err)
}
if exp := ""; storedToken != exp {
t.Errorf("expected %q to be %q", storedToken, exp)
}
})
t.Run("stores", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{
Policies: []string{"default"},
TTL: "30m",
})
if err != nil {
t.Fatal(err)
}
token := secret.Auth.ClientToken
_, cmd := testLoginCommand(t)
cmd.client = client
tokenHelper, err := cmd.TokenHelper()
if err != nil {
t.Fatal(err)
}
code := cmd.Run([]string{
token,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
storedToken, err := tokenHelper.Get()
if err != nil {
t.Fatal(err)
}
if storedToken != token {
t.Errorf("expected %q to be %q", storedToken, token)
}
})
t.Run("token_only", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil {
t.Fatal(err)
}
if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{
"password": "test",
"policies": "default",
}); err != nil {
t.Fatal(err)
}
ui, cmd := testLoginCommand(t)
cmd.client = client
tokenHelper, err := cmd.TokenHelper()
if err != nil {
t.Fatal(err)
}
code := cmd.Run([]string{
"-token-only",
"-method", "userpass",
"username=test",
"password=test",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
// Verify only the token was printed
token := ui.OutputWriter.String()
if l, exp := len(token), 36; l != exp {
t.Errorf("expected token to be %d characters, was %d: %q", exp, l, token)
}
// Verify the token was not stored
if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" {
t.Fatalf("expted token to not be stored: %s: %q", err, storedToken)
}
})
t.Run("failure_no_store", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testLoginCommand(t)
cmd.client = client
tokenHelper, err := cmd.TokenHelper()
if err != nil {
t.Fatal(err)
}
code := cmd.Run([]string{
"not-a-real-token",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error authenticating: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
if storedToken, err := tokenHelper.Get(); err != nil || storedToken != "" {
t.Fatalf("expected token to not be stored: %s: %q", err, storedToken)
}
})
t.Run("wrap_auto_unwrap", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil {
t.Fatal(err)
}
if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{
"password": "test",
"policies": "default",
}); err != nil {
t.Fatal(err)
}
_, cmd := testLoginCommand(t)
cmd.client = client
// Set the wrapping ttl to 5s. We can't set this via the flag because we
// override the client object before that particular flag is parsed.
client.SetWrappingLookupFunc(func(string, string) string { return "5m" })
code := cmd.Run([]string{
"-method", "userpass",
"username=test",
"password=test",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
// Unset the wrapping
client.SetWrappingLookupFunc(func(string, string) string { return "" })
tokenHelper, err := cmd.TokenHelper()
if err != nil {
t.Fatal(err)
}
token, err := tokenHelper.Get()
if err != nil || token == "" {
t.Fatalf("expected token from helper: %s: %q", err, token)
}
client.SetToken(token)
// Ensure the resulting token is unwrapped
secret, err := client.Auth().Token().LookupSelf()
if err != nil {
t.Error(err)
}
if secret == nil {
t.Fatal("secret was nil")
}
if secret.WrapInfo != nil {
t.Errorf("expected to be unwrapped: %#v", secret)
}
})
t.Run("wrap_token_only", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil {
t.Fatal(err)
}
if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{
"password": "test",
"policies": "default",
}); err != nil {
t.Fatal(err)
}
ui, cmd := testLoginCommand(t)
cmd.client = client
// Set the wrapping ttl to 5s. We can't set this via the flag because we
// override the client object before that particular flag is parsed.
client.SetWrappingLookupFunc(func(string, string) string { return "5m" })
code := cmd.Run([]string{
"-token-only",
"-method", "userpass",
"username=test",
"password=test",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
// Unset the wrapping
client.SetWrappingLookupFunc(func(string, string) string { return "" })
tokenHelper, err := cmd.TokenHelper()
if err != nil {
t.Fatal(err)
}
storedToken, err := tokenHelper.Get()
if err != nil || storedToken != "" {
t.Fatalf("expected token to not be stored: %s: %q", err, storedToken)
}
token := strings.TrimSpace(ui.OutputWriter.String())
if token == "" {
t.Errorf("expected %q to not be %q", token, "")
}
// Ensure the resulting token is, in fact, still wrapped.
client.SetToken(token)
secret, err := client.Logical().Unwrap("")
if err != nil {
t.Error(err)
}
if secret == nil || secret.Auth == nil || secret.Auth.ClientToken == "" {
t.Fatalf("expected secret to have auth: %#v", secret)
}
})
t.Run("wrap_no_store", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
if err := client.Sys().EnableAuth("userpass", "userpass", ""); err != nil {
t.Fatal(err)
}
if _, err := client.Logical().Write("auth/userpass/users/test", map[string]interface{}{
"password": "test",
"policies": "default",
}); err != nil {
t.Fatal(err)
}
ui, cmd := testLoginCommand(t)
cmd.client = client
// Set the wrapping ttl to 5s. We can't set this via the flag because we
// override the client object before that particular flag is parsed.
client.SetWrappingLookupFunc(func(string, string) string { return "5m" })
code := cmd.Run([]string{
"-no-store",
"-method", "userpass",
"username=test",
"password=test",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
// Unset the wrapping
client.SetWrappingLookupFunc(func(string, string) string { return "" })
tokenHelper, err := cmd.TokenHelper()
if err != nil {
t.Fatal(err)
}
storedToken, err := tokenHelper.Get()
if err != nil || storedToken != "" {
t.Fatalf("expected token to not be stored: %s: %q", err, storedToken)
}
expected := "wrapping_token"
output := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(output, expected) {
t.Errorf("expected %q to contain %q", output, expected)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testLoginCommand(t)
cmd.client = client
code := cmd.Run([]string{
"token",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error authenticating: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
// Deprecations
// TODO: remove in 0.9.0
t.Run("deprecated_no_verify", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
secret, err := client.Auth().Token().Create(&api.TokenCreateRequest{
Policies: []string{"default"},
TTL: "30m",
NumUses: 1,
})
if err != nil {
t.Fatal(err)
}
token := secret.Auth.ClientToken
_, cmd := testLoginCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-no-verify",
token,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
lookup, err := client.Auth().Token().Lookup(token)
if err != nil {
t.Fatal(err)
}
// There was 1 use to start, make sure we didn't use it (verifying would
// use it).
uses, err := lookup.TokenRemainingUses()
if err != nil {
t.Fatal(err)
}
if uses != 1 {
t.Errorf("expected %d to be %d", uses, 1)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testLoginCommand(t)
assertNoTabs(t, cmd)
})
}

112
command/main.go Normal file
View file

@ -0,0 +1,112 @@
package command
import (
"bytes"
"fmt"
"io"
"os"
"sort"
"strings"
"text/tabwriter"
"github.com/mitchellh/cli"
)
func Run(args []string) int {
// Handle -v shorthand
for _, arg := range args {
if arg == "--" {
break
}
if arg == "-v" || arg == "-version" || arg == "--version" {
args = []string{"version"}
break
}
}
// Calculate hidden commands from the deprecated ones
hiddenCommands := make([]string, 0, len(DeprecatedCommands)+1)
for k := range DeprecatedCommands {
hiddenCommands = append(hiddenCommands, k)
}
hiddenCommands = append(hiddenCommands, "version")
cli := &cli.CLI{
Name: "vault",
Args: args,
Commands: Commands,
HelpFunc: groupedHelpFunc(
cli.BasicHelpFunc("vault"),
),
HiddenCommands: hiddenCommands,
Autocomplete: true,
AutocompleteNoDefaultFlags: true,
}
exitCode, err := cli.Run()
if err != nil {
fmt.Fprintf(os.Stderr, "Error executing CLI: %s\n", err.Error())
return 1
}
return exitCode
}
var commonCommands = []string{
"read",
"write",
"delete",
"list",
"login",
"server",
"status",
"unwrap",
}
func groupedHelpFunc(f cli.HelpFunc) cli.HelpFunc {
return func(commands map[string]cli.CommandFactory) string {
var b bytes.Buffer
tw := tabwriter.NewWriter(&b, 0, 2, 6, ' ', 0)
fmt.Fprintf(tw, "Usage: vault <command> [args]\n\n")
fmt.Fprintf(tw, "Common commands:\n")
for _, v := range commonCommands {
printCommand(tw, v, commands[v])
}
otherCommands := make([]string, 0, len(commands))
for k := range commands {
found := false
for _, v := range commonCommands {
if k == v {
found = true
break
}
}
if !found {
otherCommands = append(otherCommands, k)
}
}
sort.Strings(otherCommands)
fmt.Fprintf(tw, "\n")
fmt.Fprintf(tw, "Other commands:\n")
for _, v := range otherCommands {
printCommand(tw, v, commands[v])
}
tw.Flush()
return strings.TrimSpace(b.String())
}
}
func printCommand(w io.Writer, name string, cmdFn cli.CommandFactory) {
cmd, err := cmdFn()
if err != nil {
panic(fmt.Sprintf("failed to load %q command: %s", name, err))
}
fmt.Fprintf(w, " %s\t%s\n", name, cmd.Synopsis())
}

View file

@ -1,169 +0,0 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/meta"
"github.com/posener/complete"
)
// MountCommand is a Command that mounts a new mount.
type MountCommand struct {
meta.Meta
}
func (c *MountCommand) Run(args []string) int {
var description, path, defaultLeaseTTL, maxLeaseTTL, pluginName string
var local, forceNoCache, sealWrap bool
flags := c.Meta.FlagSet("mount", meta.FlagSetDefault)
flags.StringVar(&description, "description", "", "")
flags.StringVar(&path, "path", "", "")
flags.StringVar(&defaultLeaseTTL, "default-lease-ttl", "", "")
flags.StringVar(&maxLeaseTTL, "max-lease-ttl", "", "")
flags.StringVar(&pluginName, "plugin-name", "", "")
flags.BoolVar(&forceNoCache, "force-no-cache", false, "")
flags.BoolVar(&local, "local", false, "")
flags.BoolVar(&sealWrap, "seal-wrap", false, "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) != 1 {
flags.Usage()
c.Ui.Error(fmt.Sprintf(
"\nmount expects one argument: the type to mount."))
return 1
}
mountType := args[0]
// If no path is specified, we default the path to the backend type
// or use the plugin name if it's a plugin backend
if path == "" {
if mountType == "plugin" {
path = pluginName
} else {
path = mountType
}
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 2
}
mountInfo := &api.MountInput{
Type: mountType,
Description: description,
Config: api.MountConfigInput{
DefaultLeaseTTL: defaultLeaseTTL,
MaxLeaseTTL: maxLeaseTTL,
ForceNoCache: forceNoCache,
PluginName: pluginName,
},
Local: local,
SealWrap: sealWrap,
}
if err := client.Sys().Mount(path, mountInfo); err != nil {
c.Ui.Error(fmt.Sprintf(
"Mount error: %s", err))
return 2
}
mountTypeOutput := fmt.Sprintf("'%s'", mountType)
if mountType == "plugin" {
mountTypeOutput = fmt.Sprintf("plugin '%s'", pluginName)
}
c.Ui.Output(fmt.Sprintf(
"Successfully mounted %s at '%s'!",
mountTypeOutput, path))
return 0
}
func (c *MountCommand) Synopsis() string {
return "Mount a logical backend"
}
func (c *MountCommand) Help() string {
helpText := `
Usage: vault mount [options] type
Mount a logical backend.
This command mounts a logical backend for storing and/or generating
secrets.
General Options:
` + meta.GeneralOptionsUsage() + `
Mount Options:
-description=<desc> Human-friendly description of the purpose for
the mount. This shows up in the mounts command.
-path=<path> Mount point for the logical backend. This
defaults to the type of the mount.
-default-lease-ttl=<duration> Default lease time-to-live for this backend.
If not specified, uses the global default, or
the previously set value. Set to '0' to
explicitly set it to use the global default.
-max-lease-ttl=<duration> Max lease time-to-live for this backend.
If not specified, uses the global default, or
the previously set value. Set to '0' to
explicitly set it to use the global default.
-force-no-cache Forces the backend to disable caching. If not
specified, uses the global default. This does
not affect caching of the underlying encrypted
data storage.
-plugin-name Name of the plugin to mount based from the name
in the plugin catalog.
-local Mark the mount as a local mount. Local mounts
are not replicated nor (if a secondary)
removed by replication.
-seal-wrap Turn on seal wrapping for the mount.
`
return strings.TrimSpace(helpText)
}
func (c *MountCommand) AutocompleteArgs() complete.Predictor {
// This list does not contain deprecated backends
return complete.PredictSet(
"aws",
"consul",
"pki",
"transit",
"ssh",
"rabbitmq",
"database",
"totp",
"plugin",
)
}
func (c *MountCommand) AutocompleteFlags() complete.Flags {
return complete.Flags{
"-description": complete.PredictNothing,
"-path": complete.PredictNothing,
"-default-lease-ttl": complete.PredictNothing,
"-max-lease-ttl": complete.PredictNothing,
"-force-no-cache": complete.PredictNothing,
"-plugin-name": complete.PredictNothing,
"-local": complete.PredictNothing,
"-seal-wrap": complete.PredictNothing,
}
}

View file

@ -1,90 +0,0 @@
package command
import (
"testing"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func TestMount(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &MountCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
},
}
args := []string{
"-address", addr,
"kv",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
client, err := c.Client()
if err != nil {
t.Fatalf("err: %s", err)
}
mounts, err := client.Sys().ListMounts()
if err != nil {
t.Fatalf("err: %s", err)
}
mount, ok := mounts["kv/"]
if !ok {
t.Fatal("should have kv mount")
}
if mount.Type != "kv" {
t.Fatal("should be kv type")
}
}
func TestMount_Generic(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &MountCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
},
}
args := []string{
"-address", addr,
"generic",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
client, err := c.Client()
if err != nil {
t.Fatalf("err: %s", err)
}
mounts, err := client.Sys().ListMounts()
if err != nil {
t.Fatalf("err: %s", err)
}
mount, ok := mounts["generic/"]
if !ok {
t.Fatal("should have generic mount path")
}
if mount.Type != "generic" {
t.Fatal("should be generic type")
}
}

View file

@ -1,89 +0,0 @@
package command
import (
"fmt"
"strings"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/meta"
)
// MountTuneCommand is a Command that remounts a mounted secret backend
// to a new endpoint.
type MountTuneCommand struct {
meta.Meta
}
func (c *MountTuneCommand) Run(args []string) int {
var defaultLeaseTTL, maxLeaseTTL string
flags := c.Meta.FlagSet("mount-tune", meta.FlagSetDefault)
flags.StringVar(&defaultLeaseTTL, "default-lease-ttl", "", "")
flags.StringVar(&maxLeaseTTL, "max-lease-ttl", "", "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) != 1 {
flags.Usage()
c.Ui.Error(fmt.Sprintf(
"\nmount-tune expects one arguments: the mount path"))
return 1
}
path := args[0]
mountConfig := api.MountConfigInput{
DefaultLeaseTTL: defaultLeaseTTL,
MaxLeaseTTL: maxLeaseTTL,
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 2
}
if err := client.Sys().TuneMount(path, mountConfig); err != nil {
c.Ui.Error(fmt.Sprintf(
"Mount tune error: %s", err))
return 2
}
c.Ui.Output(fmt.Sprintf(
"Successfully tuned mount '%s'!", path))
return 0
}
func (c *MountTuneCommand) Synopsis() string {
return "Tune mount configuration parameters"
}
func (c *MountTuneCommand) Help() string {
helpText := `
Usage: vault mount-tune [options] path
Tune configuration options for a mounted secret backend.
Example: vault mount-tune -default-lease-ttl="24h" secret
General Options:
` + meta.GeneralOptionsUsage() + `
Mount Options:
-default-lease-ttl=<duration> Default lease time-to-live for this backend.
If not specified, uses the system default, or
the previously set value. Set to 'system' to
explicitly set it to use the system default.
-max-lease-ttl=<duration> Max lease time-to-live for this backend.
If not specified, uses the system default, or
the previously set value. Set to 'system' to
explicitly set it to use the system default.
`
return strings.TrimSpace(helpText)
}

View file

@ -1,98 +0,0 @@
package command
import (
"fmt"
"sort"
"strconv"
"strings"
"github.com/hashicorp/vault/meta"
"github.com/ryanuber/columnize"
)
// MountsCommand is a Command that lists the mounts.
type MountsCommand struct {
meta.Meta
}
func (c *MountsCommand) Run(args []string) int {
flags := c.Meta.FlagSet("mounts", meta.FlagSetDefault)
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 2
}
mounts, err := client.Sys().ListMounts()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error reading mounts: %s", err))
return 2
}
paths := make([]string, 0, len(mounts))
for path := range mounts {
paths = append(paths, path)
}
sort.Strings(paths)
columns := []string{"Path | Type | Accessor | Plugin | Default TTL | Max TTL | Force No Cache | Replication Behavior | Seal Wrap | Description"}
for _, path := range paths {
mount := mounts[path]
pluginName := "n/a"
if mount.Config.PluginName != "" {
pluginName = mount.Config.PluginName
}
defTTL := "system"
switch {
case mount.Type == "system", mount.Type == "cubbyhole", mount.Type == "identity":
defTTL = "n/a"
case mount.Config.DefaultLeaseTTL != 0:
defTTL = strconv.Itoa(mount.Config.DefaultLeaseTTL)
}
maxTTL := "system"
switch {
case mount.Type == "system", mount.Type == "cubbyhole", mount.Type == "identity":
maxTTL = "n/a"
case mount.Config.MaxLeaseTTL != 0:
maxTTL = strconv.Itoa(mount.Config.MaxLeaseTTL)
}
replicatedBehavior := "replicated"
if mount.Local {
replicatedBehavior = "local"
}
columns = append(columns, fmt.Sprintf(
"%s | %s | %s | %s | %s | %s | %v | %s | %t | %s", path, mount.Type, mount.Accessor, pluginName, defTTL, maxTTL,
mount.Config.ForceNoCache, replicatedBehavior, mount.SealWrap, mount.Description))
}
c.Ui.Output(columnize.SimpleFormat(columns))
return 0
}
func (c *MountsCommand) Synopsis() string {
return "Lists mounted backends in Vault"
}
func (c *MountsCommand) Help() string {
helpText := `
Usage: vault mounts [options]
Outputs information about the mounted backends.
This command lists the mounted backends, their mount points, the
configured TTLs, and a human-friendly description of the mount point.
A TTL of 'system' indicates that the system default is being used.
General Options:
` + meta.GeneralOptionsUsage()
return strings.TrimSpace(helpText)
}

View file

@ -1,31 +0,0 @@
package command
import (
"testing"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func TestMounts(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &MountsCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
},
}
args := []string{
"-address", addr,
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}

47
command/operator.go Normal file
View file

@ -0,0 +1,47 @@
package command
import (
"strings"
"github.com/mitchellh/cli"
)
var _ cli.Command = (*OperatorCommand)(nil)
type OperatorCommand struct {
*BaseCommand
}
func (c *OperatorCommand) Synopsis() string {
return "Perform operator-specific tasks"
}
func (c *OperatorCommand) Help() string {
helpText := `
Usage: vault operator <subcommand> [options] [args]
This command groups subcommands for operators interacting with Vault. Most
users will not need to interact with these commands. Here are a few examples
of the operator commands:
Initialize a new Vault cluster:
$ vault operator init
Force a Vault to resign leadership in a cluster:
$ vault operator step-down
Rotate Vault's underlying encryption key:
$ vault operator rotate
Please see the individual subcommand help for detailed usage information.
`
return strings.TrimSpace(helpText)
}
func (c *OperatorCommand) Run(args []string) int {
return cli.RunResultHelp
}

View file

@ -0,0 +1,448 @@
package command
import (
"bytes"
"crypto/rand"
"encoding/base64"
"fmt"
"io"
"os"
"strings"
"github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/password"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/hashicorp/vault/helper/xor"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*OperatorGenerateRootCommand)(nil)
var _ cli.CommandAutocomplete = (*OperatorGenerateRootCommand)(nil)
type OperatorGenerateRootCommand struct {
*BaseCommand
flagInit bool
flagCancel bool
flagStatus bool
flagDecode string
flagOTP string
flagPGPKey string
flagNonce string
flagGenerateOTP bool
// Deprecation
// TODO: remove in 0.9.0
flagGenOTP bool
testStdin io.Reader // for tests
}
func (c *OperatorGenerateRootCommand) Synopsis() string {
return "Generates a new root token"
}
func (c *OperatorGenerateRootCommand) Help() string {
helpText := `
Usage: vault operator generate-root [options] [KEY]
Generates a new root token by combining a quorum of share holders. One of
the following must be provided to start the root token generation:
- A base64-encoded one-time-password (OTP) provided via the "-otp" flag.
Use the "-generate-otp" flag to generate a usable value. The resulting
token is XORed with this value when it is returned. Use the "-decode"
flag to output the final value.
- A file containing a PGP key or a keybase username in the "-pgp-key"
flag. The resulting token is encrypted with this public key.
An unseal key may be provided directly on the command line as an argument to
the command. If key is specified as "-", the command will read from stdin. If
a TTY is available, the command will prompt for text.
Generate an OTP code for the final token:
$ vault operator generate-root -generate-otp
Start a root token generation:
$ vault operator generate-root -init -otp="..."
$ vault operator generate-root -init -pgp-key="..."
Enter an unseal key to progress root token generation:
$ vault operator generate-root -otp="..."
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *OperatorGenerateRootCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP)
f := set.NewFlagSet("Command Options")
f.BoolVar(&BoolVar{
Name: "init",
Target: &c.flagInit,
Default: false,
EnvVar: "",
Completion: complete.PredictNothing,
Usage: "Start a root token generation. This can only be done if " +
"there is not currently one in progress.",
})
f.BoolVar(&BoolVar{
Name: "cancel",
Target: &c.flagCancel,
Default: false,
EnvVar: "",
Completion: complete.PredictNothing,
Usage: "Reset the root token generation progress. This will discard any " +
"submitted unseal keys or configuration.",
})
f.BoolVar(&BoolVar{
Name: "status",
Target: &c.flagStatus,
Default: false,
EnvVar: "",
Completion: complete.PredictNothing,
Usage: "Print the status of the current attempt without providing an " +
"unseal key.",
})
f.StringVar(&StringVar{
Name: "decode",
Target: &c.flagDecode,
Default: "",
EnvVar: "",
Completion: complete.PredictAnything,
Usage: "Decode and output the generated root token. This option requires " +
"the \"-otp\" flag be set to the OTP used during initialization.",
})
f.BoolVar(&BoolVar{
Name: "generate-otp",
Target: &c.flagGenerateOTP,
Default: false,
EnvVar: "",
Completion: complete.PredictNothing,
Usage: "Generate and print a high-entropy one-time-password (OTP) " +
"suitable for use with the \"-init\" flag.",
})
f.StringVar(&StringVar{
Name: "otp",
Target: &c.flagOTP,
Default: "",
EnvVar: "",
Completion: complete.PredictAnything,
Usage: "OTP code to use with \"-decode\" or \"-init\".",
})
f.VarFlag(&VarFlag{
Name: "pgp-key",
Value: (*pgpkeys.PubKeyFileFlag)(&c.flagPGPKey),
Default: "",
EnvVar: "",
Completion: complete.PredictAnything,
Usage: "Path to a file on disk containing a binary or base64-encoded " +
"public GPG key. This can also be specified as a Keybase username " +
"using the format \"keybase:<username>\". When supplied, the generated " +
"root token will be encrypted and base64-encoded with the given public " +
"key.",
})
f.StringVar(&StringVar{
Name: "nonce",
Target: &c.flagNonce,
Default: "",
EnvVar: "",
Completion: complete.PredictAnything,
Usage: "Nonce value provided at initialization. The same nonce value " +
"must be provided with each unseal key.",
})
// Deprecations: prefer longer-form, descriptive flags
// TODO: remove in 0.9.0
f.BoolVar(&BoolVar{
Name: "genotp", // -generate-otp
Target: &c.flagGenOTP,
Default: false,
Hidden: true,
})
return set
}
func (c *OperatorGenerateRootCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *OperatorGenerateRootCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *OperatorGenerateRootCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
if len(args) > 1 {
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0-1, got %d)", len(args)))
return 1
}
// Deprecations
// TODO: remove in 0.9.0
switch {
case c.flagGenOTP:
c.UI.Warn(wrapAtLength(
"The -gen-otp flag is deprecated. Please use the -generate-otp flag " +
"instead."))
c.flagGenerateOTP = c.flagGenOTP
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
switch {
case c.flagGenerateOTP:
return c.generateOTP()
case c.flagDecode != "":
return c.decode(c.flagDecode, c.flagOTP)
case c.flagCancel:
return c.cancel(client)
case c.flagInit:
return c.init(client, c.flagOTP, c.flagPGPKey)
case c.flagStatus:
return c.status(client)
default:
// If there are no other flags, prompt for an unseal key.
key := ""
if len(args) > 0 {
key = strings.TrimSpace(args[0])
}
return c.provide(client, key)
}
}
// verifyOTP verifies the given OTP code is exactly 16 bytes.
func (c *OperatorGenerateRootCommand) verifyOTP(otp string) error {
if len(otp) == 0 {
return fmt.Errorf("No OTP passed in")
}
otpBytes, err := base64.StdEncoding.DecodeString(otp)
if err != nil {
return fmt.Errorf("Error decoding base64 OTP value: %s", err)
}
if otpBytes == nil || len(otpBytes) != 16 {
return fmt.Errorf("Decoded OTP value is invalid or wrong length")
}
return nil
}
// generateOTP generates a suitable OTP code for generating a root token.
func (c *OperatorGenerateRootCommand) generateOTP() int {
buf := make([]byte, 16)
readLen, err := rand.Read(buf)
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading random bytes: %s", err))
return 2
}
if readLen != 16 {
c.UI.Error(fmt.Sprintf("Read %d bytes when we should have read 16", readLen))
return 2
}
return PrintRaw(c.UI, base64.StdEncoding.EncodeToString(buf))
}
// decode decodes the given value using the otp.
func (c *OperatorGenerateRootCommand) decode(encoded, otp string) int {
if encoded == "" {
c.UI.Error("Missing encoded value: use -decode=<string> to supply it")
return 1
}
if otp == "" {
c.UI.Error("Missing otp: use -otp to supply it")
return 1
}
tokenBytes, err := xor.XORBase64(encoded, otp)
if err != nil {
c.UI.Error(fmt.Sprintf("Error xoring token: %s", err))
return 1
}
token, err := uuid.FormatUUID(tokenBytes)
if err != nil {
c.UI.Error(fmt.Sprintf("Error formatting base64 token value: %s", err))
return 1
}
return PrintRaw(c.UI, strings.TrimSpace(token))
}
// init is used to start the generation process
func (c *OperatorGenerateRootCommand) init(client *api.Client, otp string, pgpKey string) int {
// Validate incoming fields. Either OTP OR PGP keys must be supplied.
switch {
case otp == "" && pgpKey == "":
c.UI.Error("Error initializing: must specify either -otp or -pgp-key")
return 1
case otp != "" && pgpKey != "":
c.UI.Error("Error initializing: cannot specify both -otp and -pgp-key")
return 1
case otp != "":
if err := c.verifyOTP(otp); err != nil {
c.UI.Error(fmt.Sprintf("Error initializing: invalid OTP: %s", err))
return 1
}
case pgpKey != "":
// OK
}
// Start the root generation
status, err := client.Sys().GenerateRootInit(otp, pgpKey)
if err != nil {
c.UI.Error(fmt.Sprintf("Error initializing root generation: %s", err))
return 2
}
return c.printStatus(status)
}
// provide prompts the user for the seal key and posts it to the update root
// endpoint. If this is the last unseal, this function outputs it.
func (c *OperatorGenerateRootCommand) provide(client *api.Client, key string) int {
status, err := client.Sys().GenerateRootStatus()
if err != nil {
c.UI.Error(fmt.Sprintf("Error getting root generation status: %s", err))
return 2
}
// Verify a root token generation is in progress. If there is not one in
// progress, return an error instructing the user to start one.
if !status.Started {
c.UI.Error(wrapAtLength(
"No root generation is in progress. Start a root generation by " +
"running \"vault generate-root -init\"."))
return 1
}
var nonce string
switch key {
case "-": // Read from stdin
nonce = c.flagNonce
// Pull our fake stdin if needed
stdin := (io.Reader)(os.Stdin)
if c.testStdin != nil {
stdin = c.testStdin
}
var buf bytes.Buffer
if _, err := io.Copy(&buf, stdin); err != nil {
c.UI.Error(fmt.Sprintf("Failed to read from stdin: %s", err))
return 1
}
key = buf.String()
case "": // Prompt using the tty
// Nonce value is not required if we are prompting via the terminal
nonce = status.Nonce
w := getWriterFromUI(c.UI)
fmt.Fprintf(w, "Root generation operation nonce: %s\n", nonce)
fmt.Fprintf(w, "Unseal Key (will be hidden): ")
key, err = password.Read(os.Stdin)
fmt.Fprintf(w, "\n")
if err != nil {
if err == password.ErrInterrupted {
c.UI.Error("user canceled")
return 1
}
c.UI.Error(wrapAtLength(fmt.Sprintf("An error occurred attempting to "+
"ask for the unseal key. The raw error message is shown below, but "+
"usually this is because you attempted to pipe a value into the "+
"command or you are executing outside of a terminal (tty). If you "+
"want to pipe the value, pass \"-\" as the argument to read from "+
"stdin. The raw error was: %s", err)))
return 1
}
default: // Supplied directly as an arg
nonce = c.flagNonce
}
// Trim any whitespace from they key, especially since we might have prompted
// the user for it.
key = strings.TrimSpace(key)
// Verify we have a nonce value
if nonce == "" {
c.UI.Error("Missing nonce value: specify it via the -nonce flag")
return 1
}
// Provide the key, this may potentially complete the update
status, err = client.Sys().GenerateRootUpdate(key, nonce)
if err != nil {
c.UI.Error(fmt.Sprintf("Error posting unseal key: %s", err))
return 2
}
return c.printStatus(status)
}
// cancel cancels the root token generation
func (c *OperatorGenerateRootCommand) cancel(client *api.Client) int {
if err := client.Sys().GenerateRootCancel(); err != nil {
c.UI.Error(fmt.Sprintf("Error canceling root token generation: %s", err))
return 2
}
c.UI.Output("Success! Root token generation canceled (if it was started)")
return 0
}
// status is used just to fetch and dump the status
func (c *OperatorGenerateRootCommand) status(client *api.Client) int {
status, err := client.Sys().GenerateRootStatus()
if err != nil {
c.UI.Error(fmt.Sprintf("Error getting root generation status: %s", err))
return 2
}
return c.printStatus(status)
}
// printStatus dumps the status to output
func (c *OperatorGenerateRootCommand) printStatus(status *api.GenerateRootStatusResponse) int {
out := []string{}
out = append(out, fmt.Sprintf("Nonce | %s", status.Nonce))
out = append(out, fmt.Sprintf("Started | %t", status.Started))
out = append(out, fmt.Sprintf("Progress | %d/%d", status.Progress, status.Required))
out = append(out, fmt.Sprintf("Complete | %t", status.Complete))
if status.PGPFingerprint != "" {
out = append(out, fmt.Sprintf("PGP Fingerprint | %s", status.PGPFingerprint))
}
if status.EncodedRootToken != "" {
out = append(out, fmt.Sprintf("Root Token | %s", status.EncodedRootToken))
}
output := columnOutput(out, nil)
c.UI.Output(output)
return 0
}

View file

@ -0,0 +1,448 @@
package command
import (
"io"
"regexp"
"strings"
"testing"
uuid "github.com/hashicorp/go-uuid"
"github.com/hashicorp/vault/helper/xor"
"github.com/mitchellh/cli"
)
func testOperatorGenerateRootCommand(tb testing.TB) (*cli.MockUi, *OperatorGenerateRootCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &OperatorGenerateRootCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestOperatorGenerateRootCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"init_no_args",
[]string{
"-init",
},
"must specify either -otp or -pgp-key",
1,
},
{
"init_invalid_otp",
[]string{
"-init",
"-otp", "not-a-valid-otp",
},
"Error initializing: invalid OTP:",
1,
},
{
"init_pgp_multi",
[]string{
"-init",
"-pgp-key", "keybase:hashicorp",
"-pgp-key", "keybase:jefferai",
},
"can only be specified once",
1,
},
{
"init_pgp_multi_inline",
[]string{
"-init",
"-pgp-key", "keybase:hashicorp,keybase:jefferai",
},
"can only specify one pgp key",
1,
},
{
"init_pgp_otp",
[]string{
"-init",
"-pgp-key", "keybase:hashicorp",
"-otp", "abcd1234",
},
"cannot specify both -otp and -pgp-key",
1,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ui, cmd := testOperatorGenerateRootCommand(t)
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("generate_otp", func(t *testing.T) {
t.Parallel()
ui, cmd := testOperatorGenerateRootCommand(t)
code := cmd.Run([]string{
"-generate-otp",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
output := ui.OutputWriter.String() + ui.ErrorWriter.String()
if err := cmd.verifyOTP(output); err != nil {
t.Fatal(err)
}
})
t.Run("decode", func(t *testing.T) {
t.Parallel()
encoded := "L9MaZ/4mQanpOV6QeWd84g=="
otp := "dIeeezkjpDUv3fy7MYPOLQ=="
ui, cmd := testOperatorGenerateRootCommand(t)
code := cmd.Run([]string{
"-decode", encoded,
"-otp", otp,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "5b54841c-c705-e59c-c6e4-a22b48e4b2cf"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if combined != expected {
t.Errorf("expected %q to be %q", combined, expected)
}
})
t.Run("cancel", func(t *testing.T) {
t.Parallel()
otp := "dIeeezkjpDUv3fy7MYPOLQ=="
client, closer := testVaultServer(t)
defer closer()
// Initialize a generation
if _, err := client.Sys().GenerateRootInit(otp, ""); err != nil {
t.Fatal(err)
}
ui, cmd := testOperatorGenerateRootCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-cancel",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Success! Root token generation canceled"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
status, err := client.Sys().GenerateRootStatus()
if err != nil {
t.Fatal(err)
}
if status.Started {
t.Errorf("expected status to be canceled: %#v", status)
}
})
t.Run("init_otp", func(t *testing.T) {
t.Parallel()
otp := "dIeeezkjpDUv3fy7MYPOLQ=="
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testOperatorGenerateRootCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-init",
"-otp", otp,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Nonce"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
status, err := client.Sys().GenerateRootStatus()
if err != nil {
t.Fatal(err)
}
if !status.Started {
t.Errorf("expected status to be started: %#v", status)
}
})
t.Run("init_pgp", func(t *testing.T) {
t.Parallel()
pgpKey := "keybase:hashicorp"
pgpFingerprint := "91a6e7f85d05c65630bef18951852d87348ffc4c"
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testOperatorGenerateRootCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-init",
"-pgp-key", pgpKey,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Nonce"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
status, err := client.Sys().GenerateRootStatus()
if err != nil {
t.Fatal(err)
}
if !status.Started {
t.Errorf("expected status to be started: %#v", status)
}
if status.PGPFingerprint != pgpFingerprint {
t.Errorf("expected %q to be %q", status.PGPFingerprint, pgpFingerprint)
}
})
t.Run("status", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testOperatorGenerateRootCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-status",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Nonce"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("provide_arg", func(t *testing.T) {
t.Parallel()
otp := "dIeeezkjpDUv3fy7MYPOLQ=="
client, keys, closer := testVaultServerUnseal(t)
defer closer()
// Initialize a generation
status, err := client.Sys().GenerateRootInit(otp, "")
if err != nil {
t.Fatal(err)
}
nonce := status.Nonce
// Supply the first n-1 unseal keys
for _, key := range keys[:len(keys)-1] {
_, cmd := testOperatorGenerateRootCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-nonce", nonce,
key,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
}
ui, cmd := testOperatorGenerateRootCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-nonce", nonce,
keys[len(keys)-1], // the last unseal key
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
reToken := regexp.MustCompile(`Root Token\s+(.+)`)
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
match := reToken.FindAllStringSubmatch(combined, -1)
if len(match) < 1 || len(match[0]) < 2 {
t.Fatalf("no match: %#v", match)
}
tokenBytes, err := xor.XORBase64(match[0][1], otp)
if err != nil {
t.Fatal(err)
}
token, err := uuid.FormatUUID(tokenBytes)
if err != nil {
t.Fatal(err)
}
if l, exp := len(token), 36; l != exp {
t.Errorf("expected %d to be %d: %s", l, exp, token)
}
})
t.Run("provide_stdin", func(t *testing.T) {
t.Parallel()
otp := "dIeeezkjpDUv3fy7MYPOLQ=="
client, keys, closer := testVaultServerUnseal(t)
defer closer()
// Initialize a generation
status, err := client.Sys().GenerateRootInit(otp, "")
if err != nil {
t.Fatal(err)
}
nonce := status.Nonce
// Supply the first n-1 unseal keys
for _, key := range keys[:len(keys)-1] {
stdinR, stdinW := io.Pipe()
go func() {
stdinW.Write([]byte(key))
stdinW.Close()
}()
_, cmd := testOperatorGenerateRootCommand(t)
cmd.client = client
cmd.testStdin = stdinR
code := cmd.Run([]string{
"-nonce", nonce,
"-",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
}
stdinR, stdinW := io.Pipe()
go func() {
stdinW.Write([]byte(keys[len(keys)-1])) // the last unseal key
stdinW.Close()
}()
ui, cmd := testOperatorGenerateRootCommand(t)
cmd.client = client
cmd.testStdin = stdinR
code := cmd.Run([]string{
"-nonce", nonce,
"-",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
reToken := regexp.MustCompile(`Root Token\s+(.+)`)
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
match := reToken.FindAllStringSubmatch(combined, -1)
if len(match) < 1 || len(match[0]) < 2 {
t.Fatalf("no match: %#v", match)
}
tokenBytes, err := xor.XORBase64(match[0][1], otp)
if err != nil {
t.Fatal(err)
}
token, err := uuid.FormatUUID(tokenBytes)
if err != nil {
t.Fatal(err)
}
if l, exp := len(token), 36; l != exp {
t.Errorf("expected %d to be %d: %s", l, exp, token)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testOperatorGenerateRootCommand(t)
cmd.client = client
code := cmd.Run([]string{
"secret/foo",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error getting root generation status: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testOperatorGenerateRootCommand(t)
assertNoTabs(t, cmd)
})
}

590
command/operator_init.go Normal file
View file

@ -0,0 +1,590 @@
package command
import (
"encoding/json"
"fmt"
"net/url"
"runtime"
"strings"
"github.com/ghodss/yaml"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/mitchellh/cli"
"github.com/posener/complete"
consulapi "github.com/hashicorp/consul/api"
)
var _ cli.Command = (*OperatorInitCommand)(nil)
var _ cli.CommandAutocomplete = (*OperatorInitCommand)(nil)
type OperatorInitCommand struct {
*BaseCommand
flagStatus bool
flagKeyShares int
flagKeyThreshold int
flagPGPKeys []string
flagRootTokenPGPKey string
// HSM
flagStoredShares int
flagRecoveryShares int
flagRecoveryThreshold int
flagRecoveryPGPKeys []string
// Consul
flagConsulAuto bool
flagConsulService string
// Deprecations
// TODO: remove in 0.9.0
flagAuto bool
flagCheck bool
}
func (c *OperatorInitCommand) Synopsis() string {
return "Initializes a server"
}
func (c *OperatorInitCommand) Help() string {
helpText := `
Usage: vault operator init [options]
Initializes a Vault server. Initialization is the process by which Vault's
storage backend is prepared to receive data. Since Vault server's share the
same storage backend in HA mode, you only need to initialize one Vault to
initialize the storage backend.
During initialization, Vault generates an in-memory master key and applies
Shamir's secret sharing algorithm to disassemble that master key into a
configuration number of key shares such that a configurable subset of those
key shares must come together to regenerate the master key. These keys are
often called "unseal keys" in Vault's documentation.
This command cannot be run against already-initialized Vault cluster.
Start initialization with the default options:
$ vault operator init
Initialize, but encrypt the unseal keys with pgp keys:
$ vault operator init \
-key-shares=3 \
-key-threshold=2 \
-pgp-keys="keybase:hashicorp,keybase:jefferai,keybase:sethvargo"
Encrypt the initial root token using a pgp key:
$ vault operator init -root-token-pgp-key="keybase:hashicorp"
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *OperatorInitCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP | FlagSetOutputFormat)
// Common Options
f := set.NewFlagSet("Common Options")
f.BoolVar(&BoolVar{
Name: "status",
Target: &c.flagStatus,
Default: false,
Usage: "Print the current initialization status. An exit code of 0 means " +
"the Vault is already initialized. An exit code of 1 means an error " +
"occurred. An exit code of 2 means the mean is not initialized.",
})
f.IntVar(&IntVar{
Name: "key-shares",
Aliases: []string{"n"},
Target: &c.flagKeyShares,
Default: 5,
Completion: complete.PredictAnything,
Usage: "Number of key shares to split the generated master key into. " +
"This is the number of \"unseal keys\" to generate.",
})
f.IntVar(&IntVar{
Name: "key-threshold",
Aliases: []string{"t"},
Target: &c.flagKeyThreshold,
Default: 3,
Completion: complete.PredictAnything,
Usage: "Number of key shares required to reconstruct the master key. " +
"This must be less than or equal to -key-shares.",
})
f.VarFlag(&VarFlag{
Name: "pgp-keys",
Value: (*pgpkeys.PubKeyFilesFlag)(&c.flagPGPKeys),
Completion: complete.PredictAnything,
Usage: "Comma-separated list of paths to files on disk containing " +
"public GPG keys OR a comma-separated list of Keybase usernames using " +
"the format \"keybase:<username>\". When supplied, the generated " +
"unseal keys will be encrypted and base64-encoded in the order " +
"specified in this list. The number of entires must match -key-shares, " +
"unless -store-shares are used.",
})
f.VarFlag(&VarFlag{
Name: "root-token-pgp-key",
Value: (*pgpkeys.PubKeyFileFlag)(&c.flagRootTokenPGPKey),
Completion: complete.PredictAnything,
Usage: "Path to a file on disk containing a binary or base64-encoded " +
"public GPG key. This can also be specified as a Keybase username " +
"using the format \"keybase:<username>\". When supplied, the generated " +
"root token will be encrypted and base64-encoded with the given public " +
"key.",
})
// Consul Options
f = set.NewFlagSet("Consul Options")
f.BoolVar(&BoolVar{
Name: "consul-auto",
Target: &c.flagConsulAuto,
Default: false,
Usage: "Perform automatic service discovery using Consul in HA mode. " +
"When all nodes in a Vault HA cluster are registered with Consul, " +
"enabling this option will trigger automatic service discovery based " +
"on the provided -consul-service value. When Consul is Vault's HA " +
"backend, this functionality is automatically enabled. Ensure the " +
"proper Consul environment variables are set (CONSUL_HTTP_ADDR, etc). " +
"When only one Vault server is discovered, it will be initialized " +
"automatically. When more than one Vault server is discovered, they " +
"will each be output for selection.",
})
f.StringVar(&StringVar{
Name: "consul-service",
Target: &c.flagConsulService,
Default: "vault",
Completion: complete.PredictAnything,
Usage: "Name of the service in Consul under which the Vault servers are " +
"registered.",
})
// HSM Options
f = set.NewFlagSet("HSM Options")
f.IntVar(&IntVar{
Name: "recovery-shares",
Target: &c.flagRecoveryShares,
Default: 5,
Completion: complete.PredictAnything,
Usage: "Number of key shares to split the recovery key into. " +
"This is only used in HSM mode.",
})
f.IntVar(&IntVar{
Name: "recovery-threshold",
Target: &c.flagRecoveryThreshold,
Default: 3,
Completion: complete.PredictAnything,
Usage: "Number of key shares required to reconstruct the recovery key. " +
"This is only used in HSM mode.",
})
f.VarFlag(&VarFlag{
Name: "recovery-pgp-keys",
Value: (*pgpkeys.PubKeyFilesFlag)(&c.flagRecoveryPGPKeys),
Completion: complete.PredictAnything,
Usage: "Behaves like -pgp-keys, but for the recovery key shares. This " +
"is only used in HSM mode.",
})
f.IntVar(&IntVar{
Name: "stored-shares",
Target: &c.flagStoredShares,
Default: 0, // No default, because we need to check if was supplied
Completion: complete.PredictAnything,
Usage: "Number of unseal keys to store on an HSM. This must be equal to " +
"-key-shares. This is only used in HSM mode.",
})
// Deprecations
// TODO: remove in 0.9.0
f.BoolVar(&BoolVar{
Name: "check", // prefer -status
Target: &c.flagCheck,
Default: false,
Hidden: true,
Usage: "",
})
f.BoolVar(&BoolVar{
Name: "auto", // prefer -consul-auto
Target: &c.flagAuto,
Default: false,
Hidden: true,
Usage: "",
})
return set
}
func (c *OperatorInitCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *OperatorInitCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *OperatorInitCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
// Deprecations
// TODO: remove in 0.9.0
if c.flagAuto {
c.UI.Warn(wrapAtLength("WARNING! -auto is deprecated. Please use " +
"-consul-auto instead. This will be removed the next major release " +
"of Vault."))
c.flagConsulAuto = true
}
if c.flagCheck {
c.UI.Warn(wrapAtLength("WARNING! -check is deprecated. Please use " +
"-status instead. This will be removed in the next major release " +
"of Vault."))
c.flagStatus = true
}
// Build the initial init request
initReq := &api.InitRequest{
SecretShares: c.flagKeyShares,
SecretThreshold: c.flagKeyThreshold,
PGPKeys: c.flagPGPKeys,
RootTokenPGPKey: c.flagRootTokenPGPKey,
StoredShares: c.flagStoredShares,
RecoveryShares: c.flagRecoveryShares,
RecoveryThreshold: c.flagRecoveryThreshold,
RecoveryPGPKeys: c.flagRecoveryPGPKeys,
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
// Check auto mode
switch {
case c.flagStatus:
return c.status(client)
case c.flagConsulAuto:
return c.consulAuto(client, initReq)
default:
return c.init(client, initReq)
}
}
// consulAuto enables auto-joining via Consul.
func (c *OperatorInitCommand) consulAuto(client *api.Client, req *api.InitRequest) int {
// Capture the client original address and reset it
originalAddr := client.Address()
defer client.SetAddress(originalAddr)
// Create a client to communicate with Consul
consulClient, err := consulapi.NewClient(consulapi.DefaultConfig())
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to create Consul client:%v", err))
return 1
}
// Pull the scheme from the Vault client to determine if the Consul agent
// should talk via HTTP or HTTPS.
addr := client.Address()
clientURL, err := url.Parse(addr)
if err != nil || clientURL == nil {
c.UI.Error(fmt.Sprintf("Failed to parse Vault address %s: %s", addr, err))
return 1
}
var uninitedVaults []string
var initedVault string
// Query the nodes belonging to the cluster
services, _, err := consulClient.Catalog().Service(c.flagConsulService, "", &consulapi.QueryOptions{
AllowStale: true,
})
if err == nil {
for _, service := range services {
// Set the address on the client temporarily
vaultAddr := (&url.URL{
Scheme: clientURL.Scheme,
Host: fmt.Sprintf("%s:%d", service.ServiceAddress, service.ServicePort),
}).String()
client.SetAddress(vaultAddr)
// Check the initialization status of the discovered node
inited, err := client.Sys().InitStatus()
if err != nil {
c.UI.Error(fmt.Sprintf("Error checking init status of %q: %s", vaultAddr, err))
}
if inited {
initedVault = vaultAddr
break
}
// If we got this far, we communicated successfully with Vault, but it
// was not initialized.
uninitedVaults = append(uninitedVaults, vaultAddr)
}
}
// Get the correct export keywords and quotes for *nix vs Windows
export := "export"
quote := "\""
if runtime.GOOS == "windows" {
export = "set"
quote = ""
}
if initedVault != "" {
vaultURL, err := url.Parse(initedVault)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to parse Vault address %q: %s", initedVault, err))
return 2
}
vaultAddr := vaultURL.String()
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Discovered an initialized Vault node at %q with Consul service name "+
"%q. Set the following environment variable to target the discovered "+
"Vault server:",
vaultURL.String(), c.flagConsulService)))
c.UI.Output("")
c.UI.Output(fmt.Sprintf(" $ %s VAULT_ADDR=%s%s%s", export, quote, vaultAddr, quote))
c.UI.Output("")
return 0
}
switch len(uninitedVaults) {
case 0:
c.UI.Error(fmt.Sprintf("No Vault nodes registered as %q in Consul", c.flagConsulService))
return 2
case 1:
// There was only one node found in the Vault cluster and it was
// uninitialized.
vaultURL, err := url.Parse(uninitedVaults[0])
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to parse Vault address %q: %s", initedVault, err))
return 2
}
vaultAddr := vaultURL.String()
// Update the client to connect to this Vault server
client.SetAddress(vaultAddr)
// Let the client know that initialization is perfomed on the
// discovered node.
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Discovered an initialized Vault node at %q with Consul service name "+
"%q. Set the following environment variable to target the discovered "+
"Vault server:",
vaultURL.String(), c.flagConsulService)))
c.UI.Output("")
c.UI.Output(fmt.Sprintf(" $ %s VAULT_ADDR=%s%s%s", export, quote, vaultAddr, quote))
c.UI.Output("")
c.UI.Output("Attempting to initialize it...")
c.UI.Output("")
// Attempt to initialize it
return c.init(client, req)
default:
// If more than one Vault node were discovered, print out all of them,
// requiring the client to update VAULT_ADDR and to run init again.
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Discovered %d uninitialized Vault servers with Consul service name "+
"%q. To initialize these Vatuls, set any one of the following "+
"environment variables and run \"vault init\":",
len(uninitedVaults), c.flagConsulService)))
c.UI.Output("")
// Print valid commands to make setting the variables easier
for _, node := range uninitedVaults {
vaultURL, err := url.Parse(node)
if err != nil {
c.UI.Error(fmt.Sprintf("Failed to parse Vault address %q: %s", initedVault, err))
return 2
}
vaultAddr := vaultURL.String()
c.UI.Output(fmt.Sprintf(" $ %s VAULT_ADDR=%s%s%s", export, quote, vaultAddr, quote))
}
c.UI.Output("")
return 0
}
}
func (c *OperatorInitCommand) init(client *api.Client, req *api.InitRequest) int {
resp, err := client.Sys().Init(req)
if err != nil {
c.UI.Error(fmt.Sprintf("Error initializing: %s", err))
return 2
}
switch c.flagFormat {
case "yaml", "yml":
return c.initOutputYAML(req, resp)
case "json":
return c.initOutputJSON(req, resp)
case "table":
default:
c.UI.Error(fmt.Sprintf("Unknown format: %s", c.flagFormat))
return 1
}
for i, key := range resp.Keys {
if resp.KeysB64 != nil && len(resp.KeysB64) == len(resp.Keys) {
c.UI.Output(fmt.Sprintf("Unseal Key %d: %s", i+1, resp.KeysB64[i]))
} else {
c.UI.Output(fmt.Sprintf("Unseal Key %d: %s", i+1, key))
}
}
for i, key := range resp.RecoveryKeys {
if resp.RecoveryKeysB64 != nil && len(resp.RecoveryKeysB64) == len(resp.RecoveryKeys) {
c.UI.Output(fmt.Sprintf("Recovery Key %d: %s", i+1, resp.RecoveryKeysB64[i]))
} else {
c.UI.Output(fmt.Sprintf("Recovery Key %d: %s", i+1, key))
}
}
c.UI.Output("")
c.UI.Output(fmt.Sprintf("Initial Root Token: %s", resp.RootToken))
if req.StoredShares < 1 {
c.UI.Output("")
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Vault initialized with %d key shares an a key threshold of %d. Please "+
"securely distributed the key shares printed above. When the Vault is "+
"re-sealed, restarted, or stopped, you must supply at least %d of "+
"these keys to unseal it before it can start servicing requests.",
req.SecretShares,
req.SecretThreshold,
req.SecretThreshold)))
c.UI.Output("")
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Vault does not store the generated master key. Without at least %d "+
"key to reconstruct the master key, Vault will remain permanently "+
"sealed!",
req.SecretThreshold)))
c.UI.Output("")
c.UI.Output(wrapAtLength(
"It is possible to generate new unseal keys, provided you have a quorum " +
"of existing unseal keys shares. See \"vault rekey\" for more " +
"information."))
} else {
c.UI.Output("")
c.UI.Output("Success! Vault is initialized")
}
if len(resp.RecoveryKeys) > 0 {
c.UI.Output("")
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Recovery key initialized with %d key shares and a key threshold of %d. "+
"Please securely distribute the key shares printed above.",
req.RecoveryShares,
req.RecoveryThreshold)))
}
return 0
}
// initOutputYAML outputs the init output as YAML.
func (c *OperatorInitCommand) initOutputYAML(req *api.InitRequest, resp *api.InitResponse) int {
b, err := yaml.Marshal(newMachineInit(req, resp))
if err != nil {
c.UI.Error(fmt.Sprintf("Error marshaling YAML: %s", err))
return 2
}
return PrintRaw(c.UI, strings.TrimSpace(string(b)))
}
// initOutputJSON outputs the init output as JSON.
func (c *OperatorInitCommand) initOutputJSON(req *api.InitRequest, resp *api.InitResponse) int {
b, err := json.Marshal(newMachineInit(req, resp))
if err != nil {
c.UI.Error(fmt.Sprintf("Error marshaling JSON: %s", err))
return 2
}
return PrintRaw(c.UI, strings.TrimSpace(string(b)))
}
// status inspects the init status of vault and returns an appropriate error
// code and message.
func (c *OperatorInitCommand) status(client *api.Client) int {
inited, err := client.Sys().InitStatus()
if err != nil {
c.UI.Error(fmt.Sprintf("Error checking init status: %s", err))
return 1 // Normally we'd return 2, but 2 means something special here
}
if inited {
c.UI.Output("Vault is initialized")
return 0
}
c.UI.Output("Vault is not initialized")
return 2
}
// machineInit is used to output information about the init command.
type machineInit struct {
UnsealKeysB64 []string `json:"unseal_keys_b64"`
UnsealKeysHex []string `json:"unseal_keys_hex"`
UnsealShares int `json:"unseal_shares"`
UnsealThreshold int `json:"unseal_threshold"`
RecoveryKeysB64 []string `json:"recovery_keys_b64"`
RecoveryKeysHex []string `json:"recovery_keys_hex"`
RecoveryShares int `json:"recovery_keys_shares"`
RecoveryThreshold int `json:"recovery_keys_threshold"`
RootToken string `json:"root_token"`
}
func newMachineInit(req *api.InitRequest, resp *api.InitResponse) *machineInit {
init := &machineInit{}
init.UnsealKeysHex = make([]string, len(resp.Keys))
for i, v := range resp.Keys {
init.UnsealKeysHex[i] = v
}
init.UnsealKeysB64 = make([]string, len(resp.KeysB64))
for i, v := range resp.KeysB64 {
init.UnsealKeysB64[i] = v
}
init.UnsealShares = req.SecretShares
init.UnsealThreshold = req.SecretThreshold
init.RecoveryKeysHex = make([]string, len(resp.RecoveryKeys))
for i, v := range resp.RecoveryKeys {
init.RecoveryKeysHex[i] = v
}
init.RecoveryKeysB64 = make([]string, len(resp.RecoveryKeysB64))
for i, v := range resp.RecoveryKeysB64 {
init.RecoveryKeysB64[i] = v
}
init.RecoveryShares = req.RecoveryShares
init.RecoveryThreshold = req.RecoveryThreshold
init.RootToken = resp.RootToken
return init
}

View file

@ -0,0 +1,361 @@
package command
import (
"fmt"
"os"
"regexp"
"strconv"
"strings"
"testing"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/mitchellh/cli"
)
func testOperatorInitCommand(tb testing.TB) (*cli.MockUi, *OperatorInitCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &OperatorInitCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestOperatorInitCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"pgp_keys_multi",
[]string{
"-pgp-keys", "keybase:hashicorp",
"-pgp-keys", "keybase:jefferai",
},
"can only be specified once",
1,
},
{
"root_token_pgp_key_multi",
[]string{
"-root-token-pgp-key", "keybase:hashicorp",
"-root-token-pgp-key", "keybase:jefferai",
},
"can only be specified once",
1,
},
{
"root_token_pgp_key_multi_inline",
[]string{
"-root-token-pgp-key", "keybase:hashicorp,keybase:jefferai",
},
"can only specify one pgp key",
1,
},
{
"recovery_pgp_keys_multi",
[]string{
"-recovery-pgp-keys", "keybase:hashicorp",
"-recovery-pgp-keys", "keybase:jefferai",
},
"can only be specified once",
1,
},
{
"key_shares_pgp_less",
[]string{
"-key-shares", "10",
"-pgp-keys", "keybase:jefferai,keybase:sethvargo",
},
"incorrect number",
2,
},
{
"key_shares_pgp_more",
[]string{
"-key-shares", "1",
"-pgp-keys", "keybase:jefferai,keybase:sethvargo",
},
"incorrect number",
2,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testOperatorInitCommand(t)
cmd.client = client
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("status", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerUninit(t)
defer closer()
ui, cmd := testOperatorInitCommand(t)
cmd.client = client
// Verify the non-init response code
code := cmd.Run([]string{
"-status",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
// Now init to verify the init response code
if _, err := client.Sys().Init(&api.InitRequest{
SecretShares: 1,
SecretThreshold: 1,
}); err != nil {
t.Fatal(err)
}
// Verify the init response code
ui, cmd = testOperatorInitCommand(t)
cmd.client = client
code = cmd.Run([]string{
"-status",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
})
t.Run("default", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerUninit(t)
defer closer()
ui, cmd := testOperatorInitCommand(t)
cmd.client = client
code := cmd.Run([]string{})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
init, err := client.Sys().InitStatus()
if err != nil {
t.Fatal(err)
}
if !init {
t.Error("expected initialized")
}
re := regexp.MustCompile(`Unseal Key \d+: (.+)`)
output := ui.OutputWriter.String()
match := re.FindAllStringSubmatch(output, -1)
if len(match) < 5 || len(match[0]) < 2 {
t.Fatalf("no match: %#v", match)
}
keys := make([]string, len(match))
for i := range match {
keys[i] = match[i][1]
}
// Try unsealing with those keys - only use 3, which is the default
// threshold.
for i, key := range keys[:3] {
resp, err := client.Sys().Unseal(key)
if err != nil {
t.Fatal(err)
}
exp := (i + 1) % 3 // 1, 2, 0
if resp.Progress != exp {
t.Errorf("expected %d to be %d", resp.Progress, exp)
}
}
status, err := client.Sys().SealStatus()
if err != nil {
t.Fatal(err)
}
if status.Sealed {
t.Errorf("expected vault to be unsealed: %#v", status)
}
})
t.Run("custom_shares_threshold", func(t *testing.T) {
t.Parallel()
keyShares, keyThreshold := 20, 15
client, closer := testVaultServerUninit(t)
defer closer()
ui, cmd := testOperatorInitCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-key-shares", strconv.Itoa(keyShares),
"-key-threshold", strconv.Itoa(keyThreshold),
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
init, err := client.Sys().InitStatus()
if err != nil {
t.Fatal(err)
}
if !init {
t.Error("expected initialized")
}
re := regexp.MustCompile(`Unseal Key \d+: (.+)`)
output := ui.OutputWriter.String()
match := re.FindAllStringSubmatch(output, -1)
if len(match) < keyShares || len(match[0]) < 2 {
t.Fatalf("no match: %#v", match)
}
keys := make([]string, len(match))
for i := range match {
keys[i] = match[i][1]
}
// Try unsealing with those keys - only use 3, which is the default
// threshold.
for i, key := range keys[:keyThreshold] {
resp, err := client.Sys().Unseal(key)
if err != nil {
t.Fatal(err)
}
exp := (i + 1) % keyThreshold
if resp.Progress != exp {
t.Errorf("expected %d to be %d", resp.Progress, exp)
}
}
status, err := client.Sys().SealStatus()
if err != nil {
t.Fatal(err)
}
if status.Sealed {
t.Errorf("expected vault to be unsealed: %#v", status)
}
})
t.Run("pgp", func(t *testing.T) {
t.Parallel()
tempDir, pubFiles, err := getPubKeyFiles(t)
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tempDir)
client, closer := testVaultServerUninit(t)
defer closer()
ui, cmd := testOperatorInitCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-key-shares", "4",
"-key-threshold", "2",
"-pgp-keys", fmt.Sprintf("%s,@%s, %s, %s ",
pubFiles[0], pubFiles[1], pubFiles[2], pubFiles[3]),
"-root-token-pgp-key", pubFiles[0],
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
re := regexp.MustCompile(`Unseal Key \d+: (.+)`)
output := ui.OutputWriter.String()
match := re.FindAllStringSubmatch(output, -1)
if len(match) < 4 || len(match[0]) < 2 {
t.Fatalf("no match: %#v", match)
}
keys := make([]string, len(match))
for i := range match {
keys[i] = match[i][1]
}
// Try unsealing with one key
decryptedKey := testPGPDecrypt(t, pgpkeys.TestPrivKey1, keys[0])
if _, err := client.Sys().Unseal(decryptedKey); err != nil {
t.Fatal(err)
}
// Decrypt the root token
reToken := regexp.MustCompile(`Root Token: (.+)`)
match = reToken.FindAllStringSubmatch(output, -1)
if len(match) < 1 || len(match[0]) < 2 {
t.Fatalf("no match")
}
root := match[0][1]
decryptedRoot := testPGPDecrypt(t, pgpkeys.TestPrivKey1, root)
if l, exp := len(decryptedRoot), 36; l != exp {
t.Errorf("expected %d to be %d", l, exp)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testOperatorInitCommand(t)
cmd.client = client
code := cmd.Run([]string{
"secret/foo",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error initializing: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testOperatorInitCommand(t)
assertNoTabs(t, cmd)
})
}

View file

@ -0,0 +1,74 @@
package command
import (
"fmt"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*OperatorKeyStatusCommand)(nil)
var _ cli.CommandAutocomplete = (*OperatorKeyStatusCommand)(nil)
type OperatorKeyStatusCommand struct {
*BaseCommand
}
func (c *OperatorKeyStatusCommand) Synopsis() string {
return "Provides information about the active encryption key"
}
func (c *OperatorKeyStatusCommand) Help() string {
helpText := `
Usage: vault operator key-status [options]
Provides information about the active encryption key. Specifically,
the current key term and the key installation time.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *OperatorKeyStatusCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP)
}
func (c *OperatorKeyStatusCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *OperatorKeyStatusCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *OperatorKeyStatusCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
if len(args) > 0 {
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
status, err := client.Sys().KeyStatus()
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading key status: %s", err))
return 2
}
c.UI.Output(printKeyStatus(status))
return 0
}

View file

@ -0,0 +1,110 @@
package command
import (
"strings"
"testing"
"github.com/mitchellh/cli"
)
func testOperatorKeyStatusCommand(tb testing.TB) (*cli.MockUi, *OperatorKeyStatusCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &OperatorKeyStatusCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestOperatorKeyStatusCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"too_many_args",
[]string{"foo", "bar"},
"Too many arguments",
1,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
ui, cmd := testOperatorKeyStatusCommand(t)
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("integration", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testOperatorKeyStatusCommand(t)
cmd.client = client
code := cmd.Run([]string{})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Key Term"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testOperatorKeyStatusCommand(t)
cmd.client = client
code := cmd.Run([]string{})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error reading key status: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testOperatorKeyStatusCommand(t)
assertNoTabs(t, cmd)
})
}

638
command/operator_rekey.go Normal file
View file

@ -0,0 +1,638 @@
package command
import (
"bytes"
"fmt"
"io"
"os"
"strings"
"github.com/fatih/structs"
"github.com/hashicorp/vault/api"
"github.com/hashicorp/vault/helper/password"
"github.com/hashicorp/vault/helper/pgpkeys"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*OperatorRekeyCommand)(nil)
var _ cli.CommandAutocomplete = (*OperatorRekeyCommand)(nil)
type OperatorRekeyCommand struct {
*BaseCommand
flagCancel bool
flagInit bool
flagKeyShares int
flagKeyThreshold int
flagNonce string
flagPGPKeys []string
flagStatus bool
flagTarget string
// Backup options
flagBackup bool
flagBackupDelete bool
flagBackupRetrieve bool
// Deprecations
// TODO: remove in 0.9.0
flagDelete bool
flagRecoveryKey bool
flagRetrieve bool
testStdin io.Reader // for tests
}
func (c *OperatorRekeyCommand) Synopsis() string {
return "Generates new unseal keys"
}
func (c *OperatorRekeyCommand) Help() string {
helpText := `
Usage: vault rekey [options] [KEY]
Generates a new set of unseal keys. This can optionally change the total
number of key shares or the required threshold of those key shares to
reconstruct the master key. This operation is zero downtime, but it requires
the Vault is unsealed and a quorum of existing unseal keys are provided.
An unseal key may be provided directly on the command line as an argument to
the command. If key is specified as "-", the command will read from stdin. If
a TTY is available, the command will prompt for text.
Initialize a rekey:
$ vault operator rekey \
-init \
-key-shares=15 \
-key-threshold=9
Rekey and encrypt the resulting unseal keys with PGP:
$ vault operator rekey \
-init \
-key-shares=3 \
-key-threshold=2 \
-pgp-keys="keybase:hashicorp,keybase:jefferai,keybase:sethvargo"
Store encrypted PGP keys in Vault's core:
$ vault operator rekey \
-init \
-pgp-keys="..." \
-backup
Retrieve backed-up unseal keys:
$ vault operator rekey -backup-retrieve
Delete backed-up unseal keys:
$ vault operator rekey -backup-delete
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *OperatorRekeyCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP)
f := set.NewFlagSet("Common Options")
f.BoolVar(&BoolVar{
Name: "init",
Target: &c.flagInit,
Default: false,
Usage: "Initialize the rekeying operation. This can only be done if no " +
"rekeying operation is in progress. Customize the new number of key " +
"shares and key threshold using the -key-shares and -key-threshold " +
"flags.",
})
f.BoolVar(&BoolVar{
Name: "cancel",
Target: &c.flagCancel,
Default: false,
Usage: "Reset the rekeying progress. This will discard any submitted " +
"unseal keys or configuration.",
})
f.BoolVar(&BoolVar{
Name: "status",
Target: &c.flagStatus,
Default: false,
Usage: "Print the status of the current attempt without providing an " +
"unseal key.",
})
f.IntVar(&IntVar{
Name: "key-shares",
Aliases: []string{"n"},
Target: &c.flagKeyShares,
Default: 5,
Completion: complete.PredictAnything,
Usage: "Number of key shares to split the generated master key into. " +
"This is the number of \"unseal keys\" to generate.",
})
f.IntVar(&IntVar{
Name: "key-threshold",
Aliases: []string{"t"},
Target: &c.flagKeyThreshold,
Default: 3,
Completion: complete.PredictAnything,
Usage: "Number of key shares required to reconstruct the master key. " +
"This must be less than or equal to -key-shares.",
})
f.StringVar(&StringVar{
Name: "nonce",
Target: &c.flagNonce,
Default: "",
EnvVar: "",
Completion: complete.PredictAnything,
Usage: "Nonce value provided at initialization. The same nonce value " +
"must be provided with each unseal key.",
})
f.StringVar(&StringVar{
Name: "target",
Target: &c.flagTarget,
Default: "barrier",
EnvVar: "",
Completion: complete.PredictSet("barrier", "recovery"),
Usage: "Target for rekeying. \"recovery\" only applies when HSM support " +
"is enabled.",
})
f.VarFlag(&VarFlag{
Name: "pgp-keys",
Value: (*pgpkeys.PubKeyFilesFlag)(&c.flagPGPKeys),
Completion: complete.PredictAnything,
Usage: "Comma-separated list of paths to files on disk containing " +
"public GPG keys OR a comma-separated list of Keybase usernames using " +
"the format \"keybase:<username>\". When supplied, the generated " +
"unseal keys will be encrypted and base64-encoded in the order " +
"specified in this list.",
})
f = set.NewFlagSet("Backup Options")
f.BoolVar(&BoolVar{
Name: "backup",
Target: &c.flagBackup,
Default: false,
Usage: "Store a backup of the current PGP encrypted unseal keys in " +
"Vault's core. The encrypted values can be recovered in the event of " +
"failure or discarded after success. See the -backup-delete and " +
"-backup-retrieve options for more information. This option only " +
"applies when the existing unseal keys were PGP encrypted.",
})
f.BoolVar(&BoolVar{
Name: "backup-delete",
Target: &c.flagBackupDelete,
Default: false,
Usage: "Delete any stored backup unseal keys.",
})
f.BoolVar(&BoolVar{
Name: "backup-retrieve",
Target: &c.flagBackupRetrieve,
Default: false,
Usage: "Retrieve the backed-up unseal keys. This option is only available " +
"if the PGP keys were provided and the backup has not been deleted.",
})
// Deprecations
// TODO: remove in 0.9.0
f.BoolVar(&BoolVar{
Name: "delete", // prefer -backup-delete
Target: &c.flagDelete,
Default: false,
Hidden: true,
Usage: "",
})
f.BoolVar(&BoolVar{
Name: "retrieve", // prefer -backup-retrieve
Target: &c.flagRetrieve,
Default: false,
Hidden: true,
Usage: "",
})
f.BoolVar(&BoolVar{
Name: "recovery-key", // prefer -target=recovery
Target: &c.flagRecoveryKey,
Default: false,
Hidden: true,
Usage: "",
})
return set
}
func (c *OperatorRekeyCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictAnything
}
func (c *OperatorRekeyCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *OperatorRekeyCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
if len(args) > 1 {
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0-1, got %d)", len(args)))
return 1
}
// Deprecations
// TODO: remove in 0.9.0
if c.flagDelete {
c.UI.Warn(wrapAtLength(
"WARNING! The -delete flag is deprecated. Please use -backup-delete " +
"instead. This flag will be removed in the next major release of " +
"Vault."))
c.flagBackupDelete = true
}
if c.flagRetrieve {
c.UI.Warn(wrapAtLength(
"WARNING! The -retrieve flag is deprecated. Please use -backup-retrieve " +
"instead. This flag will be removed in the next major release of " +
"Vault."))
c.flagBackupRetrieve = true
}
if c.flagRecoveryKey {
c.UI.Warn(wrapAtLength(
"WARNING! The -recovery-key flag is deprecated. Please use -target=recovery " +
"instead. This flag will be removed in the next major release of " +
"Vault."))
c.flagTarget = "recovery"
}
// Create the client
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
switch {
case c.flagBackupDelete:
return c.backupDelete(client)
case c.flagBackupRetrieve:
return c.backupRetrieve(client)
case c.flagCancel:
return c.cancel(client)
case c.flagInit:
return c.init(client)
case c.flagStatus:
return c.status(client)
default:
// If there are no other flags, prompt for an unseal key.
key := ""
if len(args) > 0 {
key = strings.TrimSpace(args[0])
}
return c.provide(client, key)
}
}
// init starts the rekey process.
func (c *OperatorRekeyCommand) init(client *api.Client) int {
// Handle the different API requests
var fn func(*api.RekeyInitRequest) (*api.RekeyStatusResponse, error)
switch strings.ToLower(strings.TrimSpace(c.flagTarget)) {
case "barrier":
fn = client.Sys().RekeyInit
case "recovery", "hsm":
fn = client.Sys().RekeyRecoveryKeyInit
default:
c.UI.Error(fmt.Sprintf("Unknown target: %s", c.flagTarget))
return 1
}
// Make the request
status, err := fn(&api.RekeyInitRequest{
SecretShares: c.flagKeyShares,
SecretThreshold: c.flagKeyThreshold,
PGPKeys: c.flagPGPKeys,
Backup: c.flagBackup,
})
if err != nil {
c.UI.Error(fmt.Sprintf("Error initializing rekey: %s", err))
return 2
}
// Print warnings about recovery, etc.
if len(c.flagPGPKeys) == 0 {
c.UI.Warn(wrapAtLength(
"WARNING! If you lose the keys after they are returned, there is no " +
"recovery. Consider canceling this operation and re-initializing " +
"with the -pgp-keys flag to protect the returned unseal keys along " +
"with -backup to allow recovery of the encrypted keys in case of " +
"emergency. You can delete the stored keys later using the -delete " +
"flag."))
c.UI.Output("")
}
if len(c.flagPGPKeys) > 0 && !c.flagBackup {
c.UI.Warn(wrapAtLength(
"WARNING! You are using PGP keys for encrypted the resulting unseal " +
"keys, but you did not enable the option to backup the keys to " +
"Vault's core. If you lose the encrypted keys after they are " +
"returned, you will not be able to recover them. Consider canceling " +
"this operation and re-running with -backup to allow recovery of the " +
"encrypted unseal keys in case of emergency. You can delete the " +
"stored keys later using the -delete flag."))
c.UI.Output("")
}
// Provide the current status
return c.printStatus(status)
}
// cancel is used to abort the rekey process.
func (c *OperatorRekeyCommand) cancel(client *api.Client) int {
// Handle the different API requests
var fn func() error
switch strings.ToLower(strings.TrimSpace(c.flagTarget)) {
case "barrier":
fn = client.Sys().RekeyCancel
case "recovery", "hsm":
fn = client.Sys().RekeyRecoveryKeyCancel
default:
c.UI.Error(fmt.Sprintf("Unknown target: %s", c.flagTarget))
return 1
}
// Make the request
if err := fn(); err != nil {
c.UI.Error(fmt.Sprintf("Error canceling rekey: %s", err))
return 2
}
c.UI.Output("Success! Canceled rekeying (if it was started)")
return 0
}
// provide prompts the user for the seal key and posts it to the update root
// endpoint. If this is the last unseal, this function outputs it.
func (c *OperatorRekeyCommand) provide(client *api.Client, key string) int {
var statusFn func() (*api.RekeyStatusResponse, error)
var updateFn func(string, string) (*api.RekeyUpdateResponse, error)
switch strings.ToLower(strings.TrimSpace(c.flagTarget)) {
case "barrier":
statusFn = client.Sys().RekeyStatus
updateFn = client.Sys().RekeyUpdate
case "recovery", "hsm":
statusFn = client.Sys().RekeyRecoveryKeyStatus
updateFn = client.Sys().RekeyRecoveryKeyUpdate
default:
c.UI.Error(fmt.Sprintf("Unknown target: %s", c.flagTarget))
return 1
}
status, err := statusFn()
if err != nil {
c.UI.Error(fmt.Sprintf("Error getting rekey status: %s", err))
return 2
}
// Verify a root token generation is in progress. If there is not one in
// progress, return an error instructing the user to start one.
if !status.Started {
c.UI.Error(wrapAtLength(
"No rekey is in progress. Start a rekey process by running " +
"\"vault rekey -init\"."))
return 1
}
var nonce string
switch key {
case "-": // Read from stdin
nonce = c.flagNonce
// Pull our fake stdin if needed
stdin := (io.Reader)(os.Stdin)
if c.testStdin != nil {
stdin = c.testStdin
}
var buf bytes.Buffer
if _, err := io.Copy(&buf, stdin); err != nil {
c.UI.Error(fmt.Sprintf("Failed to read from stdin: %s", err))
return 1
}
key = buf.String()
case "": // Prompt using the tty
// Nonce value is not required if we are prompting via the terminal
nonce = status.Nonce
w := getWriterFromUI(c.UI)
fmt.Fprintf(w, "Rekey operation nonce: %s\n", nonce)
fmt.Fprintf(w, "Unseal Key (will be hidden): ")
key, err = password.Read(os.Stdin)
fmt.Fprintf(w, "\n")
if err != nil {
if err == password.ErrInterrupted {
c.UI.Error("user canceled")
return 1
}
c.UI.Error(wrapAtLength(fmt.Sprintf("An error occurred attempting to "+
"ask for the unseal key. The raw error message is shown below, but "+
"usually this is because you attempted to pipe a value into the "+
"command or you are executing outside of a terminal (tty). If you "+
"want to pipe the value, pass \"-\" as the argument to read from "+
"stdin. The raw error was: %s", err)))
return 1
}
default: // Supplied directly as an arg
nonce = c.flagNonce
}
// Trim any whitespace from they key, especially since we might have
// prompted the user for it.
key = strings.TrimSpace(key)
// Verify we have a nonce value
if nonce == "" {
c.UI.Error("Missing nonce value: specify it via the -nonce flag")
return 1
}
// Provide the key, this may potentially complete the update
resp, err := updateFn(key, nonce)
if err != nil {
c.UI.Error(fmt.Sprintf("Error posting unseal key: %s", err))
return 2
}
if !resp.Complete {
return c.status(client)
}
return c.printUnsealKeys(status, resp)
}
// status is used just to fetch and dump the status.
func (c *OperatorRekeyCommand) status(client *api.Client) int {
// Handle the different API requests
var fn func() (*api.RekeyStatusResponse, error)
switch strings.ToLower(strings.TrimSpace(c.flagTarget)) {
case "barrier":
fn = client.Sys().RekeyStatus
case "recovery", "hsm":
fn = client.Sys().RekeyRecoveryKeyStatus
default:
c.UI.Error(fmt.Sprintf("Unknown target: %s", c.flagTarget))
return 1
}
// Make the request
status, err := fn()
if err != nil {
c.UI.Error(fmt.Sprintf("Error reading rekey status: %s", err))
return 2
}
return c.printStatus(status)
}
// backupRetrieve retrieves the stored backup keys.
func (c *OperatorRekeyCommand) backupRetrieve(client *api.Client) int {
// Handle the different API requests
var fn func() (*api.RekeyRetrieveResponse, error)
switch strings.ToLower(strings.TrimSpace(c.flagTarget)) {
case "barrier":
fn = client.Sys().RekeyRetrieveBackup
case "recovery", "hsm":
fn = client.Sys().RekeyRetrieveRecoveryBackup
default:
c.UI.Error(fmt.Sprintf("Unknown target: %s", c.flagTarget))
return 1
}
// Make the request
storedKeys, err := fn()
if err != nil {
c.UI.Error(fmt.Sprintf("Error retrieving rekey stored keys: %s", err))
return 2
}
secret := &api.Secret{
Data: structs.New(storedKeys).Map(),
}
return OutputSecret(c.UI, "table", secret)
}
// backupDelete deletes the stored backup keys.
func (c *OperatorRekeyCommand) backupDelete(client *api.Client) int {
// Handle the different API requests
var fn func() error
switch strings.ToLower(strings.TrimSpace(c.flagTarget)) {
case "barrier":
fn = client.Sys().RekeyDeleteBackup
case "recovery", "hsm":
fn = client.Sys().RekeyDeleteRecoveryBackup
default:
c.UI.Error(fmt.Sprintf("Unknown target: %s", c.flagTarget))
return 1
}
// Make the request
if err := fn(); err != nil {
c.UI.Error(fmt.Sprintf("Error deleting rekey stored keys: %s", err))
return 2
}
c.UI.Output("Success! Delete stored keys (if they existed)")
return 0
}
// printStatus dumps the status to output
func (c *OperatorRekeyCommand) printStatus(status *api.RekeyStatusResponse) int {
out := []string{}
out = append(out, "Key | Value")
out = append(out, fmt.Sprintf("Nonce | %s", status.Nonce))
out = append(out, fmt.Sprintf("Started | %t", status.Started))
if status.Started {
out = append(out, fmt.Sprintf("Rekey Progress | %d/%d", status.Progress, status.Required))
out = append(out, fmt.Sprintf("New Shares | %d", status.N))
out = append(out, fmt.Sprintf("New Threshold | %d", status.T))
}
if len(status.PGPFingerprints) > 0 {
out = append(out, fmt.Sprintf("PGP Fingerprints | %s", status.PGPFingerprints))
out = append(out, fmt.Sprintf("Backup | %t", status.Backup))
}
c.UI.Output(tableOutput(out, nil))
return 0
}
func (c *OperatorRekeyCommand) printUnsealKeys(status *api.RekeyStatusResponse, resp *api.RekeyUpdateResponse) int {
// Space between the key prompt, if any, and the output
c.UI.Output("")
// Provide the keys
var haveB64 bool
if resp.KeysB64 != nil && len(resp.KeysB64) == len(resp.Keys) {
haveB64 = true
}
for i, key := range resp.Keys {
if len(resp.PGPFingerprints) > 0 {
if haveB64 {
c.UI.Output(fmt.Sprintf("Key %d fingerprint: %s; value: %s", i+1, resp.PGPFingerprints[i], resp.KeysB64[i]))
} else {
c.UI.Output(fmt.Sprintf("Key %d fingerprint: %s; value: %s", i+1, resp.PGPFingerprints[i], key))
}
} else {
if haveB64 {
c.UI.Output(fmt.Sprintf("Key %d: %s", i+1, resp.KeysB64[i]))
} else {
c.UI.Output(fmt.Sprintf("Key %d: %s", i+1, key))
}
}
}
c.UI.Output("")
c.UI.Output(fmt.Sprintf("Operation nonce: %s", resp.Nonce))
if len(resp.PGPFingerprints) > 0 && resp.Backup {
c.UI.Output("")
c.UI.Output(wrapAtLength(fmt.Sprintf(
"The encrypted unseal keys are backed up to \"core/unseal-keys-backup\"" +
"in the storage backend. Remove these keys at any time using " +
"\"vault rekey -delete-backup\". Vault does not automatically remove " +
"these keys.",
)))
}
c.UI.Output("")
c.UI.Output(wrapAtLength(fmt.Sprintf(
"Vault rekeyed with %d key shares an a key threshold of %d. Please "+
"securely distributed the key shares printed above. When the Vault is "+
"re-sealed, restarted, or stopped, you must supply at least %d of "+
"these keys to unseal it before it can start servicing requests.",
status.N,
status.T,
status.T)))
return 0
}

View file

@ -0,0 +1,515 @@
package command
import (
"io"
"reflect"
"regexp"
"strings"
"testing"
"github.com/hashicorp/vault/api"
"github.com/mitchellh/cli"
)
func testOperatorRekeyCommand(tb testing.TB) (*cli.MockUi, *OperatorRekeyCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &OperatorRekeyCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestOperatorRekeyCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"pgp_keys_multi",
[]string{
"-init",
"-pgp-keys", "keybase:hashicorp",
"-pgp-keys", "keybase:jefferai",
},
"can only be specified once",
1,
},
{
"key_shares_pgp_less",
[]string{
"-init",
"-key-shares", "10",
"-pgp-keys", "keybase:jefferai,keybase:sethvargo",
},
"incorrect number",
2,
},
{
"key_shares_pgp_more",
[]string{
"-init",
"-key-shares", "1",
"-pgp-keys", "keybase:jefferai,keybase:sethvargo",
},
"incorrect number",
2,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testOperatorRekeyCommand(t)
cmd.client = client
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("status", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testOperatorRekeyCommand(t)
cmd.client = client
// Verify the non-init response
code := cmd.Run([]string{
"-status",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
expected := "Nonce"
combined := ui.OutputWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
// Now init to verify the init response
if _, err := client.Sys().RekeyInit(&api.RekeyInitRequest{
SecretShares: 1,
SecretThreshold: 1,
}); err != nil {
t.Fatal(err)
}
// Verify the init response
ui, cmd = testOperatorRekeyCommand(t)
cmd.client = client
code = cmd.Run([]string{
"-status",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
expected = "Progress"
combined = ui.OutputWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("cancel", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
// Initialize a rekey
if _, err := client.Sys().RekeyInit(&api.RekeyInitRequest{
SecretShares: 1,
SecretThreshold: 1,
}); err != nil {
t.Fatal(err)
}
ui, cmd := testOperatorRekeyCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-cancel",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Success! Canceled rekeying"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
status, err := client.Sys().GenerateRootStatus()
if err != nil {
t.Fatal(err)
}
if status.Started {
t.Errorf("expected status to be canceled: %#v", status)
}
})
t.Run("init", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testOperatorRekeyCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-init",
"-key-shares", "1",
"-key-threshold", "1",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
expected := "Nonce"
combined := ui.OutputWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
status, err := client.Sys().RekeyStatus()
if err != nil {
t.Fatal(err)
}
if !status.Started {
t.Errorf("expected status to be started: %#v", status)
}
})
t.Run("init_pgp", func(t *testing.T) {
t.Parallel()
pgpKey := "keybase:hashicorp"
pgpFingerprints := []string{"91a6e7f85d05c65630bef18951852d87348ffc4c"}
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testOperatorRekeyCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-init",
"-key-shares", "1",
"-key-threshold", "1",
"-pgp-keys", pgpKey,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
expected := "Nonce"
combined := ui.OutputWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
status, err := client.Sys().RekeyStatus()
if err != nil {
t.Fatal(err)
}
if !status.Started {
t.Errorf("expected status to be started: %#v", status)
}
if !reflect.DeepEqual(status.PGPFingerprints, pgpFingerprints) {
t.Errorf("expected %#v to be %#v", status.PGPFingerprints, pgpFingerprints)
}
})
t.Run("provide_arg", func(t *testing.T) {
t.Parallel()
client, keys, closer := testVaultServerUnseal(t)
defer closer()
// Initialize a rekey
status, err := client.Sys().RekeyInit(&api.RekeyInitRequest{
SecretShares: 1,
SecretThreshold: 1,
})
if err != nil {
t.Fatal(err)
}
nonce := status.Nonce
// Supply the first n-1 unseal keys
for _, key := range keys[:len(keys)-1] {
ui, cmd := testOperatorRekeyCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-nonce", nonce,
key,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
}
ui, cmd := testOperatorRekeyCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-nonce", nonce,
keys[len(keys)-1], // the last unseal key
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
re := regexp.MustCompile(`Key 1: (.+)`)
output := ui.OutputWriter.String()
match := re.FindAllStringSubmatch(output, -1)
if len(match) < 1 || len(match[0]) < 2 {
t.Fatalf("bad match: %#v", match)
}
// Grab the unseal key and try to unseal
unsealKey := match[0][1]
if err := client.Sys().Seal(); err != nil {
t.Fatal(err)
}
sealStatus, err := client.Sys().Unseal(unsealKey)
if err != nil {
t.Fatal(err)
}
if sealStatus.Sealed {
t.Errorf("expected vault to be unsealed: %#v", sealStatus)
}
})
t.Run("provide_stdin", func(t *testing.T) {
t.Parallel()
client, keys, closer := testVaultServerUnseal(t)
defer closer()
// Initialize a rekey
status, err := client.Sys().RekeyInit(&api.RekeyInitRequest{
SecretShares: 1,
SecretThreshold: 1,
})
if err != nil {
t.Fatal(err)
}
nonce := status.Nonce
// Supply the first n-1 unseal keys
for _, key := range keys[:len(keys)-1] {
stdinR, stdinW := io.Pipe()
go func() {
stdinW.Write([]byte(key))
stdinW.Close()
}()
ui, cmd := testOperatorRekeyCommand(t)
cmd.client = client
cmd.testStdin = stdinR
code := cmd.Run([]string{
"-nonce", nonce,
"-",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
}
stdinR, stdinW := io.Pipe()
go func() {
stdinW.Write([]byte(keys[len(keys)-1])) // the last unseal key
stdinW.Close()
}()
ui, cmd := testOperatorRekeyCommand(t)
cmd.client = client
cmd.testStdin = stdinR
code := cmd.Run([]string{
"-nonce", nonce,
"-",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
re := regexp.MustCompile(`Key 1: (.+)`)
output := ui.OutputWriter.String()
match := re.FindAllStringSubmatch(output, -1)
if len(match) < 1 || len(match[0]) < 2 {
t.Fatalf("bad match: %#v", match)
}
// Grab the unseal key and try to unseal
unsealKey := match[0][1]
if err := client.Sys().Seal(); err != nil {
t.Fatal(err)
}
sealStatus, err := client.Sys().Unseal(unsealKey)
if err != nil {
t.Fatal(err)
}
if sealStatus.Sealed {
t.Errorf("expected vault to be unsealed: %#v", sealStatus)
}
})
t.Run("backup", func(t *testing.T) {
t.Parallel()
pgpKey := "keybase:hashicorp"
// pgpFingerprints := []string{"91a6e7f85d05c65630bef18951852d87348ffc4c"}
client, keys, closer := testVaultServerUnseal(t)
defer closer()
ui, cmd := testOperatorRekeyCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-init",
"-key-shares", "1",
"-key-threshold", "1",
"-pgp-keys", pgpKey,
"-backup",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
// Get the status for the nonce
status, err := client.Sys().RekeyStatus()
if err != nil {
t.Fatal(err)
}
nonce := status.Nonce
var combined string
// Supply the unseal keys
for _, key := range keys {
ui, cmd := testOperatorRekeyCommand(t)
cmd.client = client
code := cmd.Run([]string{
"-nonce", nonce,
key,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
// Append to our output string
combined += ui.OutputWriter.String()
}
re := regexp.MustCompile(`Key 1 fingerprint: (.+); value: (.+)`)
match := re.FindAllStringSubmatch(combined, -1)
if len(match) < 1 || len(match[0]) < 3 {
t.Fatalf("bad match: %#v", match)
}
// Grab the output fingerprint and encrypted key
fingerprint, encryptedKey := match[0][1], match[0][2]
// Get the backup
ui, cmd = testOperatorRekeyCommand(t)
cmd.client = client
code = cmd.Run([]string{
"-backup-retrieve",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
output := ui.OutputWriter.String()
if !strings.Contains(output, fingerprint) {
t.Errorf("expected %q to contain %q", output, fingerprint)
}
if !strings.Contains(output, encryptedKey) {
t.Errorf("expected %q to contain %q", output, encryptedKey)
}
// Delete the backup
ui, cmd = testOperatorRekeyCommand(t)
cmd.client = client
code = cmd.Run([]string{
"-backup-delete",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
secret, err := client.Sys().RekeyRetrieveBackup()
if err == nil {
t.Errorf("expected error: %#v", secret)
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testOperatorRekeyCommand(t)
cmd.client = client
code := cmd.Run([]string{
"secret/foo",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error getting rekey status: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testOperatorRekeyCommand(t)
assertNoTabs(t, cmd)
})
}

84
command/operator_seal.go Normal file
View file

@ -0,0 +1,84 @@
package command
import (
"fmt"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*OperatorSealCommand)(nil)
var _ cli.CommandAutocomplete = (*OperatorSealCommand)(nil)
type OperatorSealCommand struct {
*BaseCommand
}
func (c *OperatorSealCommand) Synopsis() string {
return "Seals the Vault server"
}
func (c *OperatorSealCommand) Help() string {
helpText := `
Usage: vault seal [options]
Seals the Vault server. Sealing tells the Vault server to stop responding
to any operations until it is unsealed. When sealed, the Vault server
discards its in-memory master key to unlock the data, so it is physically
blocked from responding to operations unsealed.
If an unseal is in progress, sealing the Vault will reset the unsealing
process. Users will have to re-enter their portions of the master key again.
This command does nothing if the Vault server is already sealed.
Seal the Vault server:
$ vault operator seal
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *OperatorSealCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP)
}
func (c *OperatorSealCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *OperatorSealCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *OperatorSealCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
if len(args) > 0 {
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
if err := client.Sys().Seal(); err != nil {
c.UI.Error(fmt.Sprintf("Error sealing: %s", err))
return 2
}
c.UI.Output("Success! Vault is sealed.")
return 0
}

View file

@ -0,0 +1,122 @@
package command
import (
"strings"
"testing"
"github.com/mitchellh/cli"
)
func testOperatorSealCommand(tb testing.TB) (*cli.MockUi, *OperatorSealCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &OperatorSealCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestOperatorSealCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"args",
[]string{"foo"},
"Too many arguments",
1,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testOperatorSealCommand(t)
cmd.client = client
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("integration", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testOperatorSealCommand(t)
cmd.client = client
code := cmd.Run([]string{})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Success! Vault is sealed."
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
sealStatus, err := client.Sys().SealStatus()
if err != nil {
t.Fatal(err)
}
if !sealStatus.Sealed {
t.Errorf("expected to be sealed")
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testOperatorSealCommand(t)
cmd.client = client
code := cmd.Run([]string{})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error sealing: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testOperatorSealCommand(t)
assertNoTabs(t, cmd)
})
}

View file

@ -0,0 +1,80 @@
package command
import (
"fmt"
"strings"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*OperatorStepDownCommand)(nil)
var _ cli.CommandAutocomplete = (*OperatorStepDownCommand)(nil)
type OperatorStepDownCommand struct {
*BaseCommand
}
func (c *OperatorStepDownCommand) Synopsis() string {
return "Forces Vault to resign active duty"
}
func (c *OperatorStepDownCommand) Help() string {
helpText := `
Usage: vault operator step-down [options]
Forces the Vault server at the given address to step down from active duty.
While the affected node will have a delay before attempting to acquire the
leader lock again, if no other Vault nodes acquire the lock beforehand, it
is possible for the same node to re-acquire the lock and become active
again.
Force Vault to step down as the leader:
$ vault operator step-down
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *OperatorStepDownCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP)
}
func (c *OperatorStepDownCommand) AutocompleteArgs() complete.Predictor {
return nil
}
func (c *OperatorStepDownCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *OperatorStepDownCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
if len(args) > 0 {
c.UI.Error(fmt.Sprintf("Too many arguments (expected 0, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
if err := client.Sys().StepDown(); err != nil {
c.UI.Error(fmt.Sprintf("Error stepping down: %s", err))
return 2
}
c.UI.Output(fmt.Sprintf("Success! Stepped down: %s", client.Address()))
return 0
}

View file

@ -0,0 +1,99 @@
package command
import (
"strings"
"testing"
"github.com/mitchellh/cli"
)
func testOperatorStepDownCommand(tb testing.TB) (*cli.MockUi, *OperatorStepDownCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &OperatorStepDownCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestOperatorStepDownCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"too_many_args",
[]string{"foo"},
"Too many arguments",
1,
},
{
"default",
nil,
"Success! Stepped down: ",
0,
},
}
t.Run("validations", func(t *testing.T) {
t.Parallel()
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testOperatorStepDownCommand(t)
cmd.client = client
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testOperatorStepDownCommand(t)
cmd.client = client
code := cmd.Run([]string{})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error stepping down: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testOperatorStepDownCommand(t)
assertNoTabs(t, cmd)
})
}

145
command/operator_unseal.go Normal file
View file

@ -0,0 +1,145 @@
package command
import (
"fmt"
"io"
"os"
"strings"
"github.com/hashicorp/vault/helper/password"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
var _ cli.Command = (*OperatorUnsealCommand)(nil)
var _ cli.CommandAutocomplete = (*OperatorUnsealCommand)(nil)
type OperatorUnsealCommand struct {
*BaseCommand
flagReset bool
testOutput io.Writer // for tests
}
func (c *OperatorUnsealCommand) Synopsis() string {
return "Unseals the Vault server"
}
func (c *OperatorUnsealCommand) Help() string {
helpText := `
Usage: vault operator unseal [options] [KEY]
Provide a portion of the master key to unseal a Vault server. Vault starts
in a sealed state. It cannot perform operations until it is unsealed. This
command accepts a portion of the master key (an "unseal key").
The unseal key can be supplied as an argument to the command, but this is
not recommended as the unseal key will be available in your history:
$ vault operator unseal IXyR0OJnSFobekZMMCKCoVEpT7wI6l+USMzE3IcyDyo=
Instead, run the command with no arguments and it will prompt for the key:
$ vault operator unseal
Key (will be hidden): IXyR0OJnSFobekZMMCKCoVEpT7wI6l+USMzE3IcyDyo=
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *OperatorUnsealCommand) Flags() *FlagSets {
set := c.flagSet(FlagSetHTTP)
f := set.NewFlagSet("Command Options")
f.BoolVar(&BoolVar{
Name: "reset",
Aliases: []string{},
Target: &c.flagReset,
Default: false,
EnvVar: "",
Completion: complete.PredictNothing,
Usage: "Discard any previously entered keys to the unseal process.",
})
return set
}
func (c *OperatorUnsealCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictAnything
}
func (c *OperatorUnsealCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *OperatorUnsealCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
unsealKey := ""
args = f.Args()
switch len(args) {
case 0:
// We will prompt for the unsealKey later
case 1:
unsealKey = strings.TrimSpace(args[0])
default:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
if c.flagReset {
status, err := client.Sys().ResetUnsealProcess()
if err != nil {
c.UI.Error(fmt.Sprintf("Error resetting unseal process: %s", err))
return 2
}
return OutputSealStatus(c.UI, client, status)
}
if unsealKey == "" {
// Override the output
writer := (io.Writer)(os.Stdout)
if c.testOutput != nil {
writer = c.testOutput
}
fmt.Fprintf(writer, "Unseal Key (will be hidden): ")
value, err := password.Read(os.Stdin)
fmt.Fprintf(writer, "\n")
if err != nil {
c.UI.Error(wrapAtLength(fmt.Sprintf("An error occurred attempting to "+
"ask for an unseal key. The raw error message is shown below, but "+
"usually this is because you attempted to pipe a value into the "+
"unseal command or you are executing outside of a terminal (tty). "+
"You should run the unseal command from a terminal for maximum "+
"security. If this is not an option, the unseal can be provided as "+
"the first argument to the unseal command. The raw error "+
"was:\n\n%s", err)))
return 1
}
unsealKey = strings.TrimSpace(value)
}
status, err := client.Sys().Unseal(unsealKey)
if err != nil {
c.UI.Error(fmt.Sprintf("Error unsealing: %s", err))
return 2
}
return OutputSealStatus(c.UI, client, status)
}

View file

@ -0,0 +1,140 @@
package command
import (
"io/ioutil"
"strings"
"testing"
"github.com/mitchellh/cli"
)
func testOperatorUnsealCommand(tb testing.TB) (*cli.MockUi, *OperatorUnsealCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &OperatorUnsealCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestOperatorUnsealCommand_Run(t *testing.T) {
t.Parallel()
t.Run("error_non_terminal", func(t *testing.T) {
t.Parallel()
ui, cmd := testOperatorUnsealCommand(t)
cmd.testOutput = ioutil.Discard
code := cmd.Run(nil)
if exp := 1; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "is not a terminal"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("reset", func(t *testing.T) {
t.Parallel()
client, keys, closer := testVaultServerUnseal(t)
defer closer()
// Seal so we can unseal
if err := client.Sys().Seal(); err != nil {
t.Fatal(err)
}
// Enter an unseal key
if _, err := client.Sys().Unseal(keys[0]); err != nil {
t.Fatal(err)
}
ui, cmd := testOperatorUnsealCommand(t)
cmd.client = client
cmd.testOutput = ioutil.Discard
// Reset and check output
code := cmd.Run([]string{
"-reset",
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "0/3"
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("full", func(t *testing.T) {
t.Parallel()
client, keys, closer := testVaultServerUnseal(t)
defer closer()
// Seal so we can unseal
if err := client.Sys().Seal(); err != nil {
t.Fatal(err)
}
for _, key := range keys {
ui, cmd := testOperatorUnsealCommand(t)
cmd.client = client
cmd.testOutput = ioutil.Discard
// Reset and check output
code := cmd.Run([]string{
key,
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
}
status, err := client.Sys().SealStatus()
if err != nil {
t.Fatal(err)
}
if status.Sealed {
t.Error("expected unsealed")
}
})
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testOperatorUnsealCommand(t)
cmd.client = client
code := cmd.Run([]string{
"abcd",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error unsealing: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testOperatorUnsealCommand(t)
assertNoTabs(t, cmd)
})
}

View file

@ -4,73 +4,99 @@ import (
"fmt"
"strings"
"github.com/hashicorp/vault/meta"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
// PathHelpCommand is a Command that lists the mounts.
var _ cli.Command = (*PathHelpCommand)(nil)
var _ cli.CommandAutocomplete = (*PathHelpCommand)(nil)
var pathHelpVaultSealedMessage = strings.TrimSpace(`
Error: Vault is sealed.
The "path-help" command requires the Vault to be unsealed so that the mount
points of the secret engines are known.
`)
type PathHelpCommand struct {
meta.Meta
}
func (c *PathHelpCommand) Run(args []string) int {
flags := c.Meta.FlagSet("help", meta.FlagSetDefault)
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) != 1 {
flags.Usage()
c.Ui.Error("\nhelp expects a single argument")
return 1
}
path := args[0]
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
return 2
}
help, err := client.Help(path)
if err != nil {
if strings.Contains(err.Error(), "Vault is sealed") {
c.Ui.Error(`Error: Vault is sealed.
The path-help command requires the vault to be unsealed so that
mount points of secret backends are known.`)
} else {
c.Ui.Error(fmt.Sprintf(
"Error reading help: %s", err))
}
return 1
}
c.Ui.Output(help.Help)
return 0
*BaseCommand
}
func (c *PathHelpCommand) Synopsis() string {
return "Look up the help for a path"
return "Retrieve API help for paths"
}
func (c *PathHelpCommand) Help() string {
helpText := `
Usage: vault path-help [options] path
Usage: vault path-help [options] PATH
Look up the help for a path.
Retrieves API help for paths. All endpoints in Vault provide built-in help
in markdown format. This includes system paths, secret engines, and auth
methods.
All endpoints in Vault from system paths, secret paths, and credential
providers provide built-in help. This command looks up and outputs that
help.
Get help for the thing mounted at database/:
The command requires that the vault be unsealed, because otherwise
the mount points of the backends are unknown.
$ vault path-help database/
The response object will return additional paths to retrieve help:
$ vault path-help database/roles/
Each secret engine produces different help output.
` + c.Flags().Help()
General Options:
` + meta.GeneralOptionsUsage()
return strings.TrimSpace(helpText)
}
func (c *PathHelpCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP)
}
func (c *PathHelpCommand) AutocompleteArgs() complete.Predictor {
return complete.PredictAnything // TODO: programatic way to invoke help
}
func (c *PathHelpCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *PathHelpCommand) Run(args []string) int {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.UI.Error(err.Error())
return 2
}
path := sanitizePath(args[0])
help, err := client.Help(path)
if err != nil {
if strings.Contains(err.Error(), "Vault is sealed") {
c.UI.Error(pathHelpVaultSealedMessage)
} else {
c.UI.Error(fmt.Sprintf("Error retrieving help: %s", err))
}
return 2
}
c.UI.Output(help.Help)
return 0
}

View file

@ -1,32 +1,115 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/vault/http"
"github.com/hashicorp/vault/meta"
"github.com/hashicorp/vault/vault"
"github.com/mitchellh/cli"
)
func TestHelp(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
func testPathHelpCommand(tb testing.TB) (*cli.MockUi, *PathHelpCommand) {
tb.Helper()
ui := new(cli.MockUi)
c := &PathHelpCommand{
Meta: meta.Meta{
ClientToken: token,
Ui: ui,
ui := cli.NewMockUi()
return ui, &PathHelpCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestPathHelpCommand_Run(t *testing.T) {
t.Parallel()
cases := []struct {
name string
args []string
out string
code int
}{
{
"not_enough_args",
[]string{},
"Not enough arguments",
1,
},
{
"too_many_args",
[]string{"foo", "bar"},
"Too many arguments",
1,
},
{
"not_found",
[]string{"nope/not/once/never"},
"",
2,
},
{
"kv",
[]string{"secret/"},
"The kv backend",
0,
},
{
"sys",
[]string{"sys/mounts"},
"currently mounted backends",
0,
},
}
args := []string{
"-address", addr,
"sys/mounts",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
for _, tc := range cases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPathHelpCommand(t)
cmd.client = client
code := cmd.Run(tc.args)
if code != tc.code {
t.Errorf("expected %d to be %d", code, tc.code)
}
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, tc.out) {
t.Errorf("expected %q to contain %q", combined, tc.out)
}
})
}
t.Run("communication_failure", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServerBad(t)
defer closer()
ui, cmd := testPathHelpCommand(t)
cmd.client = client
code := cmd.Run([]string{
"sys/mounts",
})
if exp := 2; code != exp {
t.Errorf("expected %d to be %d", code, exp)
}
expected := "Error retrieving help: "
combined := ui.OutputWriter.String() + ui.ErrorWriter.String()
if !strings.Contains(combined, expected) {
t.Errorf("expected %q to contain %q", combined, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testPathHelpCommand(t)
assertNoTabs(t, cmd)
})
}

View file

@ -62,6 +62,35 @@ func getPubKeyFiles(t *testing.T) (string, []string, error) {
return tempDir, pubFiles, nil
}
func testPGPDecrypt(tb testing.TB, privKey, enc string) string {
tb.Helper()
privKeyBytes, err := base64.StdEncoding.DecodeString(privKey)
if err != nil {
tb.Fatal(err)
}
ptBuf := bytes.NewBuffer(nil)
entity, err := openpgp.ReadEntity(packet.NewReader(bytes.NewBuffer(privKeyBytes)))
if err != nil {
tb.Fatal(err)
}
var rootBytes []byte
rootBytes, err = base64.StdEncoding.DecodeString(enc)
if err != nil {
tb.Fatal(err)
}
entityList := &openpgp.EntityList{entity}
md, err := openpgp.ReadMessage(bytes.NewBuffer(rootBytes), entityList, nil, nil)
if err != nil {
tb.Fatal(err)
}
ptBuf.ReadFrom(md.UnverifiedBody)
return ptBuf.String()
}
func parseDecryptAndTestUnsealKeys(t *testing.T,
input, rootToken string,
fingerprints bool,

View file

@ -0,0 +1,52 @@
package command
import (
"github.com/mitchellh/cli"
)
// Deprecation
// TODO: remove in 0.9.0
var _ cli.Command = (*PoliciesDeprecatedCommand)(nil)
type PoliciesDeprecatedCommand struct {
*BaseCommand
}
func (c *PoliciesDeprecatedCommand) Synopsis() string { return "" }
func (c *PoliciesDeprecatedCommand) Help() string {
return (&PolicyListCommand{
BaseCommand: c.BaseCommand,
}).Help()
}
func (c *PoliciesDeprecatedCommand) Run(args []string) int {
oargs := args
f := c.flagSet(FlagSetHTTP)
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = f.Args()
// Got an arg, this is trying to read a policy
if len(args) > 0 {
return (&PolicyReadCommand{
BaseCommand: &BaseCommand{
UI: c.UI,
client: c.client,
},
}).Run(oargs)
}
// No args, probably ran "vault policies" and we want to translate that to
// "vault policy list"
return (&PolicyListCommand{
BaseCommand: &BaseCommand{
UI: c.UI,
client: c.client,
},
}).Run(oargs)
}

View file

@ -0,0 +1,96 @@
package command
import (
"strings"
"testing"
"github.com/mitchellh/cli"
)
func testPoliciesDeprecatedCommand(tb testing.TB) (*cli.MockUi, *PoliciesDeprecatedCommand) {
tb.Helper()
ui := cli.NewMockUi()
return ui, &PoliciesDeprecatedCommand{
BaseCommand: &BaseCommand{
UI: ui,
},
}
}
func TestPoliciesDeprecatedCommand_Run(t *testing.T) {
t.Parallel()
// TODO: remove in 0.9.0
t.Run("deprecated_arg", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPoliciesDeprecatedCommand(t)
cmd.client = client
// vault policies ARG -> vault policy read ARG
code := cmd.Run([]string{"default"})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
stdout := ui.OutputWriter.String()
if expected := "token/"; !strings.Contains(stdout, expected) {
t.Errorf("expected %q to contain %q", stdout, expected)
}
})
t.Run("deprecated_no_args", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPoliciesDeprecatedCommand(t)
cmd.client = client
// vault policies -> vault policy list
code := cmd.Run([]string{})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
stdout := ui.OutputWriter.String()
if expected := "root"; !strings.Contains(stdout, expected) {
t.Errorf("expected %q to contain %q", stdout, expected)
}
})
t.Run("deprecated_with_flags", func(t *testing.T) {
t.Parallel()
client, closer := testVaultServer(t)
defer closer()
ui, cmd := testPoliciesDeprecatedCommand(t)
cmd.client = client
// vault policies -flag -> vault policy list
code := cmd.Run([]string{
"-address", client.Address(),
})
if exp := 0; code != exp {
t.Errorf("expected %d to be %d: %s", code, exp, ui.ErrorWriter.String())
}
stdout := ui.OutputWriter.String()
if expected := "root"; !strings.Contains(stdout, expected) {
t.Errorf("expected %q to contain %q", stdout, expected)
}
})
t.Run("no_tabs", func(t *testing.T) {
t.Parallel()
_, cmd := testPoliciesDeprecatedCommand(t)
assertNoTabs(t, cmd)
})
}

47
command/policy.go Normal file
View file

@ -0,0 +1,47 @@
package command
import (
"strings"
"github.com/mitchellh/cli"
)
var _ cli.Command = (*PolicyCommand)(nil)
// PolicyCommand is a Command that holds the audit commands
type PolicyCommand struct {
*BaseCommand
}
func (c *PolicyCommand) Synopsis() string {
return "Interact with policies"
}
func (c *PolicyCommand) Help() string {
helpText := `
Usage: vault policy <subcommand> [options] [args]
This command groups subcommands for interacting with policies. Users can
Users can write, read, and list policies in Vault.
List all enabled policies:
$ vault policy list
Create a policy named "my-policy" from contents on local disk:
$ vault policy write my-policy ./my-policy.hcl
Delete the policy named my-policy:
$ vault policy delete my-policy
Please see the individual subcommand help for detailed usage information.
`
return strings.TrimSpace(helpText)
}
func (c *PolicyCommand) Run(args []string) int {
return cli.RunResultHelp
}

View file

@ -4,62 +4,82 @@ import (
"fmt"
"strings"
"github.com/hashicorp/vault/meta"
"github.com/mitchellh/cli"
"github.com/posener/complete"
)
// PolicyDeleteCommand is a Command that enables a new endpoint.
var _ cli.Command = (*PolicyDeleteCommand)(nil)
var _ cli.CommandAutocomplete = (*PolicyDeleteCommand)(nil)
type PolicyDeleteCommand struct {
meta.Meta
*BaseCommand
}
func (c *PolicyDeleteCommand) Synopsis() string {
return "Deletes a policy by name"
}
func (c *PolicyDeleteCommand) Help() string {
helpText := `
Usage: vault policy delete [options] NAME
Deletes the policy named NAME in the Vault server. Once the policy is deleted,
all tokens associated with the policy are affected immediately.
Delete the policy named "my-policy":
$ vault policy delete my-policy
Note that it is not possible to delete the "default" or "root" policies.
These are built-in policies.
` + c.Flags().Help()
return strings.TrimSpace(helpText)
}
func (c *PolicyDeleteCommand) Flags() *FlagSets {
return c.flagSet(FlagSetHTTP)
}
func (c *PolicyDeleteCommand) AutocompleteArgs() complete.Predictor {
return c.PredictVaultPolicies()
}
func (c *PolicyDeleteCommand) AutocompleteFlags() complete.Flags {
return c.Flags().Completions()
}
func (c *PolicyDeleteCommand) Run(args []string) int {
flags := c.Meta.FlagSet("policy-delete", meta.FlagSetDefault)
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
f := c.Flags()
if err := f.Parse(args); err != nil {
c.UI.Error(err.Error())
return 1
}
args = flags.Args()
if len(args) != 1 {
flags.Usage()
c.Ui.Error(fmt.Sprintf(
"\npolicy-delete expects exactly one argument"))
args = f.Args()
switch {
case len(args) < 1:
c.UI.Error(fmt.Sprintf("Not enough arguments (expected 1, got %d)", len(args)))
return 1
case len(args) > 1:
c.UI.Error(fmt.Sprintf("Too many arguments (expected 1, got %d)", len(args)))
return 1
}
client, err := c.Client()
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error initializing client: %s", err))
c.UI.Error(err.Error())
return 2
}
name := args[0]
name := strings.TrimSpace(strings.ToLower(args[0]))
if err := client.Sys().DeletePolicy(name); err != nil {
c.Ui.Error(fmt.Sprintf(
"Error: %s", err))
return 1
c.UI.Error(fmt.Sprintf("Error deleting %s: %s", name, err))
return 2
}
c.Ui.Output(fmt.Sprintf("Policy '%s' deleted.", name))
c.UI.Output(fmt.Sprintf("Success! Deleted policy: %s", name))
return 0
}
func (c *PolicyDeleteCommand) Synopsis() string {
return "Delete a policy from the server"
}
func (c *PolicyDeleteCommand) Help() string {
helpText := `
Usage: vault policy-delete [options] name
Delete a policy with the given name.
Once the policy is deleted, all users associated with the policy will
be affected immediately. When a user is associated with a policy that
doesn't exist, it is identical to not being associated with that policy.
General Options:
` + meta.GeneralOptionsUsage()
return strings.TrimSpace(helpText)
}

Some files were not shown because too many files have changed in this diff Show more