mirror of
https://github.com/hashicorp/vault.git
synced 2026-06-09 08:55:13 -04:00
Merge pull request #3287 from hashicorp/sethvargo/cli-magic
Standardize all commands with autocomplete, help output, and tests
This commit is contained in:
commit
166db9275e
390 changed files with 30590 additions and 15071 deletions
|
|
@ -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
|
||||
|
|
|
|||
14
Makefile
14
Makefile
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
611
api/SPEC.md
611
api/SPEC.md
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
184
api/secret.go
184
api/secret.go
|
|
@ -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.
|
||||
|
|
|
|||
1972
api/secret_test.go
1972
api/secret_test.go
File diff suppressed because it is too large
Load diff
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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/]
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
166
builtin/credential/token/cli.go
Normal file
166
builtin/credential/token/cli.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
391
cli/commands.go
391
cli/commands.go
|
|
@ -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
|
||||
},
|
||||
}
|
||||
}
|
||||
82
cli/help.go
82
cli/help.go
|
|
@ -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()
|
||||
}
|
||||
53
cli/main.go
53
cli/main.go
|
|
@ -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
42
command/audit.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
602
command/auth.go
602
command/auth.go
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
125
command/auth_help.go
Normal 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
152
command/auth_help_test.go
Normal 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
167
command/auth_list.go
Normal 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
105
command/auth_list_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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
120
command/auth_tune.go
Normal 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
149
command/auth_tune_test.go
Normal 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
399
command/base.go
Normal 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
780
command/base_flags.go
Normal 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
243
command/base_helpers.go
Normal 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)
|
||||
}
|
||||
162
command/base_helpers_test.go
Normal file
162
command/base_helpers_test.go
Normal 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
417
command/base_predict.go
Normal 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
|
||||
}
|
||||
526
command/base_predict_test.go
Normal file
526
command/base_predict_test.go
Normal 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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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
960
command/commands.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
406
command/init.go
406
command/init.go
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
40
command/lease.go
Normal 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
127
command/lease_renew.go
Normal 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
170
command/lease_renew_test.go
Normal 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
142
command/lease_revoke.go
Normal 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
|
||||
}
|
||||
}
|
||||
134
command/lease_revoke_test.go
Normal file
134
command/lease_revoke_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
134
command/list.go
134
command/list.go
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
379
command/login.go
Normal 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
496
command/login_test.go
Normal 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
112
command/main.go
Normal 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())
|
||||
}
|
||||
169
command/mount.go
169
command/mount.go
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
47
command/operator.go
Normal 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
|
||||
}
|
||||
448
command/operator_generate_root.go
Normal file
448
command/operator_generate_root.go
Normal 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
|
||||
}
|
||||
448
command/operator_generate_root_test.go
Normal file
448
command/operator_generate_root_test.go
Normal 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
590
command/operator_init.go
Normal 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
|
||||
}
|
||||
361
command/operator_init_test.go
Normal file
361
command/operator_init_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
74
command/operator_key_status.go
Normal file
74
command/operator_key_status.go
Normal 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
|
||||
}
|
||||
110
command/operator_key_status_test.go
Normal file
110
command/operator_key_status_test.go
Normal 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
638
command/operator_rekey.go
Normal 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
|
||||
}
|
||||
515
command/operator_rekey_test.go
Normal file
515
command/operator_rekey_test.go
Normal 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
84
command/operator_seal.go
Normal 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
|
||||
}
|
||||
122
command/operator_seal_test.go
Normal file
122
command/operator_seal_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
80
command/operator_step_down.go
Normal file
80
command/operator_step_down.go
Normal 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
|
||||
}
|
||||
99
command/operator_step_down_test.go
Normal file
99
command/operator_step_down_test.go
Normal 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
145
command/operator_unseal.go
Normal 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)
|
||||
}
|
||||
140
command/operator_unseal_test.go
Normal file
140
command/operator_unseal_test.go
Normal 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)
|
||||
})
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
52
command/policies_deprecated.go
Normal file
52
command/policies_deprecated.go
Normal 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)
|
||||
}
|
||||
96
command/policies_deprecated_test.go
Normal file
96
command/policies_deprecated_test.go
Normal 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
47
command/policy.go
Normal 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
|
||||
}
|
||||
|
|
@ -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
Loading…
Reference in a new issue