Merge remote-tracking branch 'upstream/master' into f-pki

This commit is contained in:
Jeff Mitchell 2015-06-19 13:01:26 -04:00
commit d8ed14a603
50 changed files with 904 additions and 202 deletions

View File

@ -80,6 +80,8 @@ func TestCopy_response(t *testing.T) {
}
func TestHash(t *testing.T) {
now := time.Now().UTC()
cases := []struct {
Input interface{}
Output interface{}
@ -116,6 +118,24 @@ func TestHash(t *testing.T) {
"foo",
"foo",
},
{
&logical.Auth{
LeaseOptions: logical.LeaseOptions{
Lease: 1 * time.Hour,
LeaseIssue: now,
},
ClientToken: "foo",
},
&logical.Auth{
LeaseOptions: logical.LeaseOptions{
Lease: 1 * time.Hour,
LeaseIssue: now,
},
ClientToken: "sha1:0beec7b5ea3f0fdbc95d0dd47f3c5bc275da8a33",
},
},
}
for _, tc := range cases {

View File

@ -61,7 +61,7 @@ func (b *backend) pathLogin(
Policies: matched.Entry.Policies,
DisplayName: matched.Entry.DisplayName,
Metadata: map[string]string{
"cert_name": matched.Entry.Name,
"cert_name": matched.Entry.Name,
"common_name": connState.PeerCertificates[0].Subject.CommonName,
},
LeaseOptions: logical.LeaseOptions{
@ -187,5 +187,5 @@ func (b *backend) pathLoginRenew(
return nil, nil
}
return framework.LeaseExtend(cert.Lease, 0)(req, d)
return framework.LeaseExtend(cert.Lease, 0, false)(req, d)
}

View File

@ -77,7 +77,7 @@ func (b *backend) pathLoginRenew(
return logical.ErrorResponse("policies have changed, revoking login"), nil
}
return framework.LeaseExtend(1*time.Hour, 0)(req, d)
return framework.LeaseExtend(1*time.Hour, 0, false)(req, d)
}
const pathLoginSyn = `

View File

@ -68,7 +68,7 @@ func (b *backend) pathLoginRenew(
return nil, nil
}
return framework.LeaseExtend(1*time.Hour, 0)(req, d)
return framework.LeaseExtend(1*time.Hour, 0, false)(req, d)
}
const pathLoginSyn = `

View File

@ -115,7 +115,7 @@ func (b *backend) secretAccessKeysRenew(
lease = &configLease{Lease: 1 * time.Hour}
}
f := framework.LeaseExtend(lease.Lease, lease.LeaseMax)
f := framework.LeaseExtend(lease.Lease, lease.LeaseMax, false)
return f(req, d)
}

View File

@ -26,7 +26,7 @@ func secretToken() *framework.Secret {
DefaultDuration: DefaultLeaseDuration,
DefaultGracePeriod: DefaultGracePeriod,
Renew: framework.LeaseExtend(1*time.Hour, 0),
Renew: framework.LeaseExtend(0, 0, true),
Revoke: secretTokenRevoke,
}
}

View File

@ -44,7 +44,7 @@ func (b *backend) secretCredsRenew(
lease = &configLease{Lease: 1 * time.Hour}
}
f := framework.LeaseExtend(lease.Lease, lease.LeaseMax)
f := framework.LeaseExtend(lease.Lease, lease.LeaseMax, false)
return f(req, d)
}

View File

@ -2,7 +2,6 @@ package postgresql
import (
"fmt"
"math/rand"
"time"
"github.com/hashicorp/vault/logical"
@ -51,10 +50,15 @@ func (b *backend) pathRoleCreateRead(
lease = &configLease{Lease: 1 * time.Hour}
}
// Generate the username, password and expiration
username := fmt.Sprintf(
"vault-%s-%d-%d",
req.DisplayName, time.Now().Unix(), rand.Int31n(10000))
// Generate the username, password and expiration. PG limits user to 63 characters
displayName := req.DisplayName
if len(displayName) > 26 {
displayName = displayName[:26]
}
username := fmt.Sprintf("%s-%s", displayName, generateUUID())
if len(username) > 63 {
username = username[:63]
}
password := generateUUID()
expiration := time.Now().UTC().
Add(lease.Lease + time.Duration((float64(lease.Lease) * 0.1))).

View File

@ -58,7 +58,7 @@ func (b *backend) secretCredsRenew(
lease = &configLease{Lease: 1 * time.Hour}
}
f := framework.LeaseExtend(lease.Lease, lease.LeaseMax)
f := framework.LeaseExtend(lease.Lease, lease.LeaseMax, false)
resp, err := f(req, d)
if err != nil {
return nil, err

View File

@ -15,11 +15,13 @@ func Backend() *framework.Backend {
PathsSpecial: &logical.Paths{
Root: []string{
"keys/*",
"raw/*",
},
},
Paths: []*framework.Path{
pathKeys(),
pathRaw(),
pathEncrypt(),
pathDecrypt(),
},

View File

@ -21,10 +21,27 @@ func TestBackend_basic(t *testing.T) {
Steps: []logicaltest.TestStep{
testAccStepWritePolicy(t, "test"),
testAccStepReadPolicy(t, "test", false),
testAccStepReadRaw(t, "test", false),
testAccStepEncrypt(t, "test", testPlaintext, decryptData),
testAccStepDecrypt(t, "test", testPlaintext, decryptData),
testAccStepDeletePolicy(t, "test"),
testAccStepReadPolicy(t, "test", true),
testAccStepReadRaw(t, "test", true),
},
})
}
func TestBackend_upsert(t *testing.T) {
decryptData := make(map[string]interface{})
logicaltest.Test(t, logicaltest.TestCase{
Backend: Backend(),
Steps: []logicaltest.TestStep{
testAccStepReadPolicy(t, "test", true),
testAccStepEncrypt(t, "test", testPlaintext, decryptData),
testAccStepReadPolicy(t, "test", false),
testAccStepDecrypt(t, "test", testPlaintext, decryptData),
testAccStepDeletePolicy(t, "test"),
testAccStepReadPolicy(t, "test", true),
},
})
}
@ -65,6 +82,43 @@ func testAccStepReadPolicy(t *testing.T, name string, expectNone bool) logicalte
return err
}
if d.Name != name {
return fmt.Errorf("bad: %#v", d)
}
if d.CipherMode != "aes-gcm" {
return fmt.Errorf("bad: %#v", d)
}
// Should NOT get a key back
if d.Key != nil {
return fmt.Errorf("bad: %#v", d)
}
return nil
},
}
}
func testAccStepReadRaw(t *testing.T, name string, expectNone bool) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "raw/" + name,
Check: func(resp *logical.Response) error {
if resp == nil && !expectNone {
return fmt.Errorf("missing response")
} else if expectNone {
if resp != nil {
return fmt.Errorf("response when expecting none")
}
return nil
}
var d struct {
Name string `mapstructure:"name"`
Key []byte `mapstructure:"key"`
CipherMode string `mapstructure:"cipher_mode"`
}
if err := mapstructure.Decode(resp.Data, &d); err != nil {
return err
}
if d.Name != name {
return fmt.Errorf("bad: %#v", d)
}

View File

@ -5,6 +5,7 @@ import (
"crypto/cipher"
"crypto/rand"
"encoding/base64"
"fmt"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
@ -56,7 +57,10 @@ func pathEncryptWrite(
// Error if invalid policy
if p == nil {
return logical.ErrorResponse("policy not found"), logical.ErrInvalidRequest
p, err = generatePolicy(req.Storage, name)
if err != nil {
return logical.ErrorResponse(fmt.Sprintf("failed to upsert policy: %v", err)), logical.ErrInvalidRequest
}
}
// Guard against a potentially invalid cipher-mode

View File

@ -45,6 +45,41 @@ func getPolicy(req *logical.Request, name string) (*Policy, error) {
return p, nil
}
// generatePolicy is used to create a new named policy with
// a randomly generated key
func generatePolicy(storage logical.Storage, name string) (*Policy, error) {
// Create the policy object
p := &Policy{
Name: name,
CipherMode: "aes-gcm",
}
// Generate a 256bit key
p.Key = make([]byte, 32)
_, err := rand.Read(p.Key)
if err != nil {
return nil, err
}
// Encode the policy
buf, err := p.Serialize()
if err != nil {
return nil, err
}
// Write the policy into storage
err = storage.Put(&logical.StorageEntry{
Key: "policy/" + name,
Value: buf,
})
if err != nil {
return nil, err
}
// Return the policy
return p, nil
}
func pathKeys() *framework.Path {
return &framework.Path{
Pattern: `keys/(?P<name>\w+)`,
@ -79,34 +114,9 @@ func pathPolicyWrite(
return nil, nil
}
// Create the policy object
p := &Policy{
Name: name,
CipherMode: "aes-gcm",
}
// Generate a 256bit key
p.Key = make([]byte, 32)
_, err = rand.Read(p.Key)
if err != nil {
return nil, err
}
// Encode the policy
buf, err := p.Serialize()
if err != nil {
return nil, err
}
// Write the policy into storage
err = req.Storage.Put(&logical.StorageEntry{
Key: "policy/" + name,
Value: buf,
})
if err != nil {
return nil, err
}
return nil, nil
// Generate the policy
_, err = generatePolicy(req.Storage, name)
return nil, err
}
func pathPolicyRead(
@ -124,7 +134,6 @@ func pathPolicyRead(
resp := &logical.Response{
Data: map[string]interface{}{
"name": p.Name,
"key": p.Key,
"cipher_mode": p.CipherMode,
},
}

View File

@ -0,0 +1,54 @@
package transit
import (
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathRaw() *framework.Path {
return &framework.Path{
Pattern: `raw/(?P<name>\w+)`,
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the key",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: pathRawRead,
},
HelpSynopsis: pathPolicyHelpSyn,
HelpDescription: pathPolicyHelpDesc,
}
}
func pathRawRead(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
name := d.Get("name").(string)
p, err := getPolicy(req, name)
if err != nil {
return nil, err
}
if p == nil {
return nil, nil
}
// Return the response
resp := &logical.Response{
Data: map[string]interface{}{
"name": p.Name,
"key": p.Key,
"cipher_mode": p.CipherMode,
},
}
return resp, nil
}
const pathRawHelpSyn = `Fetch raw keys for named encrption keys`
const pathRawHelpDesc = `
This path is used to get the underlying encryption keys used for the
named keys that are available.
`

View File

@ -2,6 +2,8 @@ package cli
import (
"os"
"os/signal"
"syscall"
auditFile "github.com/hashicorp/vault/builtin/audit/file"
auditSyslog "github.com/hashicorp/vault/builtin/audit/syslog"
@ -70,11 +72,12 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
"transit": transit.Factory,
"mysql": mysql.Factory,
},
ShutdownCh: makeShutdownCh(),
}, nil
},
"help": func() (cli.Command, error) {
return &command.HelpCommand{
"path-help": func() (cli.Command, error) {
return &command.PathHelpCommand{
Meta: meta,
}, nil
},
@ -270,3 +273,20 @@ func Commands(metaPtr *command.Meta) map[string]cli.CommandFactory {
},
}
}
// makeShutdownCh returns a channel that can be used for shutdown
// notifications for commands. This channel will send a message for every
// interrupt or SIGTERM received.
func makeShutdownCh() <-chan struct{} {
resultCh := make(chan struct{})
signalCh := make(chan os.Signal, 4)
signal.Notify(signalCh, os.Interrupt, syscall.SIGTERM)
go func() {
for {
<-signalCh
resultCh <- struct{}{}
}
}()
return resultCh
}

View File

@ -13,14 +13,14 @@ import (
// 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{}{},
"help": struct{}{},
"read": struct{}{},
"renew": struct{}{},
"revoke": struct{}{},
"write": struct{}{},
"server": struct{}{},
"status": struct{}{},
"delete": struct{}{},
"path-help": struct{}{},
"read": struct{}{},
"renew": struct{}{},
"revoke": struct{}{},
"write": struct{}{},
"server": struct{}{},
"status": struct{}{},
}
// Determine the maximum key length, and classify based on type

View File

@ -114,6 +114,15 @@ func (c *AuthCommand) Run(args []string) int {
return 0
}
// Warn if the VAULT_TOKEN environment variable is set, as that will take
// precedence
if os.Getenv("VAULT_TOKEN") != "" {
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}

View File

@ -53,6 +53,16 @@ func outputFormatTable(ui cli.Ui, s *api.Secret, whitespace bool) int {
"lease_renewable %s %s", config.Delim, strconv.FormatBool(s.Renewable)))
}
if s.Auth != nil {
input = append(input, fmt.Sprintf("token %s %s", config.Delim, s.Auth.ClientToken))
input = append(input, fmt.Sprintf("token_duration %s %d", config.Delim, s.Auth.LeaseDuration))
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))
}
}
for k, v := range s.Data {
input = append(input, fmt.Sprintf("%s %s %v", k, config.Delim, v))
}

View File

@ -5,12 +5,12 @@ import (
"strings"
)
// HelpCommand is a Command that lists the mounts.
type HelpCommand struct {
// PathHelpCommand is a Command that lists the mounts.
type PathHelpCommand struct {
Meta
}
func (c *HelpCommand) Run(args []string) int {
func (c *PathHelpCommand) Run(args []string) int {
flags := c.Meta.FlagSet("help", FlagSetDefault)
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
@ -35,8 +35,15 @@ func (c *HelpCommand) Run(args []string) int {
help, err := client.Help(path)
if err != nil {
c.Ui.Error(fmt.Sprintf(
"Error reading help: %s", err))
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
}
@ -44,13 +51,13 @@ func (c *HelpCommand) Run(args []string) int {
return 0
}
func (c *HelpCommand) Synopsis() string {
func (c *PathHelpCommand) Synopsis() string {
return "Look up the help for a path"
}
func (c *HelpCommand) Help() string {
func (c *PathHelpCommand) Help() string {
helpText := `
Usage: vault help [options] path
Usage: vault path-help [options] path
Look up the help for a path.
@ -58,6 +65,9 @@ Usage: vault help [options] path
providers provide built-in help. This command looks up and outputs that
help.
The command requires that the Vault be unsealed, because otherwise
the mount points of the backends are unknown.
General Options:
-address=addr The address of the Vault server.

View File

@ -14,7 +14,7 @@ func TestHelp(t *testing.T) {
defer ln.Close()
ui := new(cli.MockUi)
c := &HelpCommand{
c := &PathHelpCommand{
Meta: Meta{
ClientToken: token,
Ui: ui,

View File

@ -22,12 +22,16 @@ func (c *ReadCommand) Run(args []string) int {
}
args = flags.Args()
if len(args) < 1 || len(args) > 2 {
c.Ui.Error("read expects one or two arguments")
if len(args) != 1 {
c.Ui.Error("read expects one argument")
flags.Usage()
return 1
}
path := args[0]
if path[0] == '/' {
path = path[1:]
}
client, err := c.Client()
if err != nil {
@ -98,7 +102,7 @@ Read Options:
-format=table The format for output. By default it is a whitespace-
delimited table. This can also be json.
-field=field If included, the raw value of the specified field
-field=field If included, the raw value of the specified field
will be output raw to stdout.
`

View File

@ -32,6 +32,7 @@ type ServerCommand struct {
CredentialBackends map[string]logical.Factory
LogicalBackends map[string]logical.Factory
ShutdownCh <-chan struct{}
Meta
}
@ -154,7 +155,7 @@ func (c *ServerCommand) Run(args []string) int {
"immediately begin using the Vault CLI.\n\n"+
"The only step you need to take is to set the following\n"+
"environment variables:\n\n"+
" export VAULT_ADDR='http://127.0.0.1:8200'\n"+
" export VAULT_ADDR='http://127.0.0.1:8200'\n\n"+
"The unseal key and root token are reproduced below in case you\n"+
"want to seal/unseal the Vault or play with authentication.\n\n"+
"Unseal Key: %s\nRoot Token: %s\n",
@ -237,7 +238,14 @@ func (c *ServerCommand) Run(args []string) int {
// Release the log gate.
logGate.Flush()
<-make(chan struct{})
// Wait for shutdown
select {
case <-c.ShutdownCh:
c.Ui.Output("==> Vault shutdown triggered")
if err := core.Shutdown(); err != nil {
c.Ui.Error(fmt.Sprintf("Error with core shutdown: %s", err))
}
}
return 0
}
@ -407,8 +415,8 @@ General Options:
specified multiple times. If it is a directory, all
files with a ".hcl" or ".json" suffix will be loaded.
-dev Enables Dev mode. In this mode, Vault is completely
in-memory and unsealed. Do not run the Dev server in
-dev Enables Dev mode. In this mode, Vault is completely
in-memory and unsealed. Do not run the Dev server in
production!
-log-level=info Log verbosity. Defaults to "info", will be outputted

View File

@ -15,12 +15,14 @@ type TokenCreateCommand struct {
}
func (c *TokenCreateCommand) Run(args []string) int {
var format string
var displayName, lease string
var orphan bool
var metadata map[string]string
var numUses int
var policies []string
flags := c.Meta.FlagSet("mount", FlagSetDefault)
flags.StringVar(&format, "format", "table", "")
flags.StringVar(&displayName, "display-name", "", "")
flags.StringVar(&lease, "lease", "", "")
flags.BoolVar(&orphan, "orphan", false, "")
@ -61,8 +63,7 @@ func (c *TokenCreateCommand) Run(args []string) int {
return 2
}
c.Ui.Output(secret.Auth.ClientToken)
return 0
return OutputSecret(c.Ui, format, secret)
}
func (c *TokenCreateCommand) Synopsis() string {
@ -121,6 +122,10 @@ Token Options:
-use-limit=5 The number of times this token can be used until
it is automatically revoked.
-format=table The format for output. By default it is a whitespace-
delimited table. This can also be json.
`
return strings.TrimSpace(helpText)
}

View File

@ -1,6 +1,7 @@
package command
import (
"strings"
"testing"
"github.com/hashicorp/vault/http"
@ -27,4 +28,10 @@ func TestTokenCreate(t *testing.T) {
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
// Ensure we get lease info
output := ui.OutputWriter.String()
if !strings.Contains(output, "token_duration") {
t.Fatalf("bad: %#v", output)
}
}

View File

@ -19,15 +19,18 @@ type WriteCommand struct {
func (c *WriteCommand) Run(args []string) int {
var format string
var force bool
flags := c.Meta.FlagSet("write", FlagSetDefault)
flags.StringVar(&format, "format", "table", "")
flags.BoolVar(&force, "force", false, "")
flags.BoolVar(&force, "f", false, "")
flags.Usage = func() { c.Ui.Error(c.Help()) }
if err := flags.Parse(args); err != nil {
return 1
}
args = flags.Args()
if len(args) < 2 {
if len(args) < 2 && !force {
c.Ui.Error("write expects at least two arguments")
flags.Usage()
return 1
@ -117,6 +120,12 @@ General Options:
not recommended. This is especially not recommended
for unsealing a vault.
Write Options:
-f | -force Force the write to continue without any data values
specified. This allows writing to keys that do not
need or expect any fields to be specified.
`
return strings.TrimSpace(helpText)
}

View File

@ -246,3 +246,26 @@ func TestWrite_Output(t *testing.T) {
t.Fatalf("bad: %s", string(ui.OutputWriter.Bytes()))
}
}
func TestWrite_force(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := http.TestServer(t, core)
defer ln.Close()
ui := new(cli.MockUi)
c := &WriteCommand{
Meta: Meta{
ClientToken: token,
Ui: ui,
},
}
args := []string{
"-address", addr,
"-force",
"sys/rotate",
}
if code := c.Run(args); code != 0 {
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
}
}

View File

@ -13,7 +13,7 @@ func handleSysMounts(core *vault.Core) http.Handler {
switch r.Method {
case "GET":
handleSysListMounts(core).ServeHTTP(w, r)
case "POST":
case "PUT", "POST":
fallthrough
case "DELETE":
handleSysMountUnmount(core, w, r)
@ -27,8 +27,7 @@ func handleSysMounts(core *vault.Core) http.Handler {
func handleSysRemount(core *vault.Core) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "POST":
case "PUT":
case "PUT", "POST":
default:
respondError(w, http.StatusMethodNotAllowed, nil)
return
@ -80,7 +79,7 @@ func handleSysListMounts(core *vault.Core) http.Handler {
func handleSysMountUnmount(core *vault.Core, w http.ResponseWriter, r *http.Request) {
switch r.Method {
case "POST":
case "PUT", "POST":
case "DELETE":
default:
respondError(w, http.StatusMethodNotAllowed, nil)
@ -100,7 +99,7 @@ func handleSysMountUnmount(core *vault.Core, w http.ResponseWriter, r *http.Requ
}
switch r.Method {
case "POST":
case "PUT", "POST":
handleSysMount(core, w, r, path)
case "DELETE":
handleSysUnmount(core, w, r, path)

View File

@ -76,6 +76,22 @@ func TestSysMount(t *testing.T) {
}
}
func TestSysMount_put(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)
defer ln.Close()
TestServerAuth(t, addr, token)
resp := testHttpPut(t, addr+"/v1/sys/mounts/foo", map[string]interface{}{
"type": "generic",
"description": "foo",
})
testResponseStatus(t, resp, 204)
// The TestSysMount test tests the thing is actually created. See that test
// for more info.
}
func TestSysRemount(t *testing.T) {
core, _, token := vault.TestCoreUnsealed(t)
ln, addr := TestServer(t, core)

View File

@ -373,6 +373,8 @@ func (t FieldType) Zero() interface{} {
return false
case TypeMap:
return map[string]interface{}{}
case TypeDurationSecond:
return 0
default:
panic("unknown type: " + t.String())
}

View File

@ -215,7 +215,7 @@ func TestBackendHandleRequest_renew(t *testing.T) {
func TestBackendHandleRequest_renewExtend(t *testing.T) {
secret := &Secret{
Type: "foo",
Renew: LeaseExtend(0, 0),
Renew: LeaseExtend(0, 0, false),
DefaultDuration: 5 * time.Minute,
}
b := &Backend{
@ -508,6 +508,16 @@ func TestFieldSchemaDefaultOrZero(t *testing.T) {
&FieldSchema{Type: TypeString},
"",
},
"default duration set": {
&FieldSchema{Type: TypeDurationSecond, Default: 60},
60,
},
"default duration not set": {
&FieldSchema{Type: TypeDurationSecond},
0,
},
}
for name, tc := range cases {

View File

@ -2,6 +2,9 @@ package framework
import (
"fmt"
"strconv"
"strings"
"time"
"github.com/mitchellh/mapstructure"
)
@ -64,13 +67,7 @@ func (d *FieldData) GetOkErr(k string) (interface{}, bool, error) {
}
switch schema.Type {
case TypeBool:
fallthrough
case TypeInt:
fallthrough
case TypeMap:
fallthrough
case TypeString:
case TypeBool, TypeInt, TypeMap, TypeDurationSecond, TypeString:
return d.getPrimitive(k, schema)
default:
return nil, false,
@ -114,6 +111,38 @@ func (d *FieldData) getPrimitive(
}
return result, true, nil
case TypeDurationSecond:
var result int
switch inp := raw.(type) {
case int:
result = inp
case float32:
result = int(inp)
case float64:
result = int(inp)
case string:
// Look for a suffix otherwise its a plain second value
if strings.HasSuffix(inp, "s") || strings.HasSuffix(inp, "m") || strings.HasSuffix(inp, "h") {
dur, err := time.ParseDuration(inp)
if err != nil {
return nil, true, err
}
result = int(dur.Seconds())
} else {
// Plain integer
val, err := strconv.ParseInt(inp, 10, 64)
if err != nil {
return nil, true, err
}
result = int(val)
}
default:
return nil, false, fmt.Errorf("invalid input '%v'", raw)
}
return result, true, nil
default:
panic(fmt.Sprintf("Unknown type: %s", schema.Type))
}

View File

@ -91,6 +91,50 @@ func TestFieldDataGet(t *testing.T) {
"child": true,
},
},
"duration type, string value": {
map[string]*FieldSchema{
"foo": &FieldSchema{Type: TypeDurationSecond},
},
map[string]interface{}{
"foo": "42",
},
"foo",
42,
},
"duration type, string duration value": {
map[string]*FieldSchema{
"foo": &FieldSchema{Type: TypeDurationSecond},
},
map[string]interface{}{
"foo": "42m",
},
"foo",
2520,
},
"duration type, int value": {
map[string]*FieldSchema{
"foo": &FieldSchema{Type: TypeDurationSecond},
},
map[string]interface{}{
"foo": 42,
},
"foo",
42,
},
"duration type, float value": {
map[string]*FieldSchema{
"foo": &FieldSchema{Type: TypeDurationSecond},
},
map[string]interface{}{
"foo": 42.0,
},
"foo",
42,
},
}
for name, tc := range cases {

View File

@ -9,6 +9,10 @@ const (
TypeInt
TypeBool
TypeMap
// TypeDurationSecond represent as seconds, this can be either an
// integer or go duration format string (e.g. 24h)
TypeDurationSecond
)
func (t FieldType) String() string {
@ -21,6 +25,8 @@ func (t FieldType) String() string {
return "bool"
case TypeMap:
return "map"
case TypeDurationSecond:
return "duration (sec)"
default:
return "unknown type"
}

View File

@ -13,63 +13,61 @@ import (
// setting it to 2 hours forces a renewal within the next 2 hours again.
//
// maxSession is the maximum session length allowed since the original
// issue time. If this is zero, it is ignored,.
func LeaseExtend(max, maxSession time.Duration) OperationFunc {
// issue time. If this is zero, it is ignored.
//
// maxFromLease controls if the maximum renewal period comes from the existing
// lease. This means the value of `max` will be replaced with the existing
// lease duration.
func LeaseExtend(max, maxSession time.Duration, maxFromLease bool) OperationFunc {
return func(req *logical.Request, data *FieldData) (*logical.Response, error) {
lease := detectLease(req)
if lease == nil {
return nil, fmt.Errorf("no lease options for request")
}
// Check if we should limit max
if maxFromLease {
max = lease.Lease
}
// Sanity check the desired increment
switch {
// Protect against negative leases
case lease.LeaseIncrement < 0:
return logical.ErrorResponse(
"increment must be greater than 0"), logical.ErrInvalidRequest
// If no lease increment, or too large of an increment, use the max
case max > 0 && lease.LeaseIncrement == 0, max > 0 && lease.LeaseIncrement > max:
lease.LeaseIncrement = max
}
// Get the current time
now := time.Now().UTC()
// Check if we're passed the issue limit
var maxSessionTime time.Time
if maxSession > 0 {
maxSessionTime = lease.LeaseIssue.Add(maxSession)
if maxSessionTime.Sub(now) <= 0 {
if maxSessionTime.Before(now) {
return logical.ErrorResponse(fmt.Sprintf(
"lease can only be renewed up to %s past original issue",
maxSession)), logical.ErrInvalidRequest
}
}
// Protect against negative leases
if lease.LeaseIncrement < 0 {
return logical.ErrorResponse(
"increment must be greater than 0"), logical.ErrInvalidRequest
}
// If the lease is zero, then assume max
if lease.LeaseIncrement == 0 {
lease.LeaseIncrement = max
}
// If the increment is greater than the amount of time we have left
// on our session, set it to that.
if !maxSessionTime.IsZero() {
diff := maxSessionTime.Sub(lease.ExpirationTime())
if diff < lease.LeaseIncrement {
lease.LeaseIncrement = diff
}
// The new lease is the minimum of the requested LeaseIncrement
// or the maxSessionTime
requestedLease := now.Add(lease.LeaseIncrement)
if !maxSessionTime.IsZero() && requestedLease.After(maxSessionTime) {
requestedLease = maxSessionTime
}
// Determine the requested lease
newLease := lease.IncrementedLease(lease.LeaseIncrement)
if max > 0 {
// Determine if the requested lease is too long
maxExpiration := now.Add(max)
newExpiration := now.Add(newLease)
if newExpiration.Sub(maxExpiration) > 0 {
// The new expiration is past the max expiration. In this
// case, admit the longest lease we can.
newLease = maxExpiration.Sub(lease.ExpirationTime())
}
}
newLeaseDuration := requestedLease.Sub(now)
// Set the lease
lease.Lease = newLease
lease.Lease = newLeaseDuration
return &logical.Response{Auth: req.Auth, Secret: req.Secret}, nil
}
}
@ -80,6 +78,5 @@ func detectLease(req *logical.Request) *logical.LeaseOptions {
} else if req.Secret != nil {
return &req.Secret.LeaseOptions
}
return nil
}

View File

@ -11,11 +11,12 @@ func TestLeaseExtend(t *testing.T) {
now := time.Now().UTC().Round(time.Hour)
cases := map[string]struct {
Max time.Duration
MaxSession time.Duration
Request time.Duration
Result time.Duration
Error bool
Max time.Duration
MaxSession time.Duration
Request time.Duration
Result time.Duration
MaxFromLease bool
Error bool
}{
"valid request, good bounds": {
Max: 30 * time.Hour,
@ -62,20 +63,26 @@ func TestLeaseExtend(t *testing.T) {
Request: -7 * time.Hour,
Error: true,
},
"max form lease, request too large": {
Request: 10 * time.Hour,
MaxFromLease: true,
Result: time.Hour,
},
}
for name, tc := range cases {
req := &logical.Request{
Auth: &logical.Auth{
LeaseOptions: logical.LeaseOptions{
Lease: 1 * time.Second,
Lease: 1 * time.Hour,
LeaseIssue: now,
LeaseIncrement: tc.Request,
},
},
}
callback := LeaseExtend(tc.Max, tc.MaxSession)
callback := LeaseExtend(tc.Max, tc.MaxSession, tc.MaxFromLease)
resp, err := callback(req, nil)
if (err != nil) != tc.Error {
t.Fatalf("bad: %s\nerr: %s", name, err)

View File

@ -46,22 +46,8 @@ func (l *LeaseOptions) LeaseTotal() time.Duration {
// ExpirationTime computes the time until expiration including the grace period
func (l *LeaseOptions) ExpirationTime() time.Time {
var expireTime time.Time
if !l.LeaseIssue.IsZero() && l.Lease > 0 {
expireTime = l.LeaseIssue.UTC().Add(l.LeaseTotal())
if l.LeaseEnabled() {
expireTime = time.Now().UTC().Add(l.LeaseTotal())
}
return expireTime
}
// IncrementedLease returns the lease duration that would need to set
// in order to increment the _current_ lease by the given duration
// if the auth were re-issued right now.
func (l *LeaseOptions) IncrementedLease(inc time.Duration) time.Duration {
var result time.Duration
expireTime := l.ExpirationTime()
if expireTime.IsZero() {
return result
}
return expireTime.Add(inc).Sub(time.Now().UTC())
}

View File

@ -5,17 +5,6 @@ import (
"time"
)
func TestLeaseOptionsIncrementedLease(t *testing.T) {
var l LeaseOptions
l.Lease = 1 * time.Second
l.LeaseIssue = time.Now().UTC()
actual := l.IncrementedLease(1 * time.Second)
if actual > 3*time.Second || actual < 1*time.Second {
t.Fatalf("bad: %s", actual)
}
}
func TestLeaseOptionsLeaseTotal(t *testing.T) {
var l LeaseOptions
l.Lease = 1 * time.Hour
@ -66,12 +55,11 @@ func TestLeaseOptionsLeaseTotal_negGrace(t *testing.T) {
func TestLeaseOptionsExpirationTime(t *testing.T) {
var l LeaseOptions
l.Lease = 1 * time.Hour
l.LeaseIssue = time.Now().UTC()
actual := l.ExpirationTime()
expected := l.LeaseIssue.Add(l.Lease)
if !actual.Equal(expected) {
t.Fatalf("bad: %s", actual)
limit := time.Now().UTC().Add(time.Hour)
exp := l.ExpirationTime()
if exp.Before(limit) {
t.Fatalf("bad: %s", exp)
}
}
@ -79,11 +67,10 @@ func TestLeaseOptionsExpirationTime_grace(t *testing.T) {
var l LeaseOptions
l.Lease = 1 * time.Hour
l.LeaseGracePeriod = 30 * time.Minute
l.LeaseIssue = time.Now().UTC()
limit := time.Now().UTC().Add(time.Hour + 30*time.Minute)
actual := l.ExpirationTime()
expected := l.LeaseIssue.Add(l.Lease + l.LeaseGracePeriod)
if !actual.Equal(expected) {
if actual.Before(limit) {
t.Fatalf("bad: %s", actual)
}
}
@ -92,11 +79,10 @@ func TestLeaseOptionsExpirationTime_graceNegative(t *testing.T) {
var l LeaseOptions
l.Lease = 1 * time.Hour
l.LeaseGracePeriod = -1 * 30 * time.Minute
l.LeaseIssue = time.Now().UTC()
limit := time.Now().UTC().Add(time.Hour)
actual := l.ExpirationTime()
expected := l.LeaseIssue.Add(l.Lease)
if !actual.Equal(expected) {
if actual.Before(limit) {
t.Fatalf("bad: %s", actual)
}
}

175
physical/mysql.go Normal file
View File

@ -0,0 +1,175 @@
package physical
import (
"database/sql"
"fmt"
"sort"
"strings"
"time"
"github.com/armon/go-metrics"
_ "github.com/go-sql-driver/mysql"
)
// MySQLBackend is a physical backend that stores data
// within MySQL database.
type MySQLBackend struct {
dbTable string
client *sql.DB
statements map[string]*sql.Stmt
}
// newMySQLBackend constructs a MySQL backend using the given API client and
// server address and credential for accessing mysql database.
func newMySQLBackend(conf map[string]string) (Backend, error) {
// Get the MySQL credentials to perform read/write operations.
username, ok := conf["username"]
if !ok || username == "" {
return nil, fmt.Errorf("missing username")
}
password, ok := conf["password"]
if !ok || username == "" {
return nil, fmt.Errorf("missing password")
}
// Get or set MySQL server address. Defaults to localhost and default port(3306)
address, ok := conf["address"]
if !ok {
address = "127.0.0.1:3306"
}
// Get the MySQL database and table details.
database, ok := conf["database"]
if !ok {
database = "vault"
}
table, ok := conf["table"]
if !ok {
table = "vault"
}
dbTable := database + "." + table
// Create MySQL handle for the database.
dsn := username + ":" + password + "@tcp(" + address + ")/"
db, err := sql.Open("mysql", dsn)
if err != nil {
return nil, fmt.Errorf("failed to connect to mysql: %v", err)
}
// Create the required database if it doesn't exists.
if _, err := db.Exec("CREATE DATABASE IF NOT EXISTS " + database); err != nil {
return nil, fmt.Errorf("failed to create mysql database: %v", err)
}
// Create the required table if it doesn't exists.
create_query := "CREATE TABLE IF NOT EXISTS " + dbTable +
" (vault_key varchar(512), vault_value mediumblob, PRIMARY KEY (vault_key))"
if _, err := db.Exec(create_query); err != nil {
return nil, fmt.Errorf("failed to create mysql table: %v", err)
}
// Setup the backend.
m := &MySQLBackend{
dbTable: dbTable,
client: db,
statements: make(map[string]*sql.Stmt),
}
// Prepare all the statements required
statements := map[string]string{
"put": "INSERT INTO " + dbTable +
" VALUES( ?, ? ) ON DUPLICATE KEY UPDATE vault_value=VALUES(vault_value)",
"get": "SELECT vault_value FROM " + dbTable + " WHERE vault_key = ?",
"delete": "DELETE FROM " + dbTable + " WHERE vault_key = ?",
"list": "SELECT vault_key FROM " + dbTable + " WHERE vault_key LIKE ?",
}
for name, query := range statements {
if err := m.prepare(name, query); err != nil {
return nil, err
}
}
return m, nil
}
// prepare is a helper to prepare a query for future execution
func (m *MySQLBackend) prepare(name, query string) error {
stmt, err := m.client.Prepare(query)
if err != nil {
return fmt.Errorf("failed to prepare '%s': %v", name, err)
}
m.statements[name] = stmt
return nil
}
// Put is used to insert or update an entry.
func (m *MySQLBackend) Put(entry *Entry) error {
defer metrics.MeasureSince([]string{"mysql", "put"}, time.Now())
_, err := m.statements["put"].Exec(entry.Key, entry.Value)
if err != nil {
return err
}
return nil
}
// Get is used to fetch and entry.
func (m *MySQLBackend) Get(key string) (*Entry, error) {
defer metrics.MeasureSince([]string{"mysql", "get"}, time.Now())
var result []byte
err := m.statements["get"].QueryRow(key).Scan(&result)
if err == sql.ErrNoRows {
return nil, nil
}
if err != nil {
return nil, err
}
ent := &Entry{
Key: key,
Value: result,
}
return ent, nil
}
// Delete is used to permanently delete an entry
func (m *MySQLBackend) Delete(key string) error {
defer metrics.MeasureSince([]string{"mysql", "delete"}, time.Now())
_, err := m.statements["delete"].Exec(key)
if err != nil {
return err
}
return nil
}
// List is used to list all the keys under a given
// prefix, up to the next prefix.
func (m *MySQLBackend) List(prefix string) ([]string, error) {
defer metrics.MeasureSince([]string{"mysql", "list"}, time.Now())
// Add the % wildcard to the prefix to do the prefix search
likePrefix := prefix + "%"
rows, err := m.statements["list"].Query(likePrefix)
var keys []string
for rows.Next() {
var key string
err = rows.Scan(&key)
if err != nil {
return nil, fmt.Errorf("failed to scan rows: %v", err)
}
key = strings.TrimPrefix(key, prefix)
if i := strings.Index(key, "/"); i == -1 {
// Add objects only from the current 'folder'
keys = append(keys, key)
} else if i != -1 {
// Add truncated 'folder' paths
keys = appendIfMissing(keys, string(key[:i+1]))
}
}
sort.Strings(keys)
return keys, nil
}

53
physical/mysql_test.go Normal file
View File

@ -0,0 +1,53 @@
package physical
import (
"os"
"testing"
_ "github.com/go-sql-driver/mysql"
)
func TestMySQLBackend(t *testing.T) {
address := os.Getenv("MYSQL_ADDR")
if address == "" {
t.SkipNow()
}
database := os.Getenv("MYSQL_DB")
if database == "" {
database = "test"
}
table := os.Getenv("MYSQL_TABLE")
if table == "" {
table = "test"
}
username := os.Getenv("MYSQL_USERNAME")
password := os.Getenv("MYSQL_PASSWORD")
// Run vault tests
b, err := NewBackend("mysql", map[string]string{
"address": address,
"database": database,
"table": table,
"username": username,
"password": password,
})
if err != nil {
t.Fatalf("Failed to create new backend: %v", err)
}
defer func() {
mysql := b.(*MySQLBackend)
_, err := mysql.client.Exec("DROP TABLE " + mysql.dbTable)
if err != nil {
t.Fatalf("Failed to drop table: %v", err)
}
}()
testBackend(t, b)
testBackend_ListPrefix(t, b)
}

View File

@ -84,4 +84,5 @@ var BuiltinBackends = map[string]Factory{
"file": newFileBackend,
"s3": newS3Backend,
"etcd": newEtcdBackend,
"mysql": newMySQLBackend,
}

View File

@ -328,6 +328,21 @@ func NewCore(conf *CoreConfig) (*Core, error) {
return c, nil
}
// Shutdown is invoked when the Vault instance is about to be terminated. It
// should not be accessible as part of an API call as it will cause an availability
// problem. It is only used to gracefully quit in the case of HA so that failover
// happens as quickly as possible.
func (c *Core) Shutdown() error {
c.stateLock.Lock()
defer c.stateLock.Unlock()
if c.sealed {
return nil
}
// Seal the Vault, causes a leader stepdown
return c.sealInternal()
}
// HandleRequest is used to handle a new incoming request
func (c *Core) HandleRequest(req *logical.Request) (resp *logical.Response, err error) {
c.stateLock.RLock()
@ -413,8 +428,9 @@ func (c *Core) handleRequest(req *logical.Request) (*logical.Response, error) {
}
// Only the token store is allowed to return an auth block, for any
// other request this is an internal error
if resp != nil && resp.Auth != nil {
// other request this is an internal error. We exclude renewal of a token,
// since it does not need to be re-registered
if resp != nil && resp.Auth != nil && !strings.HasPrefix(req.Path, "auth/token/renew/") {
if !strings.HasPrefix(req.Path, "auth/token/") {
c.logger.Printf(
"[ERR] core: unexpected Auth response for non-token backend "+
@ -929,6 +945,14 @@ func (c *Core) Seal(token string) error {
return err
}
// Seal the Vault
return c.sealInternal()
}
// sealInternal is an internal method used to seal the vault.
// It does not do any authorization checking. The stateLock must
// be held prior to calling.
func (c *Core) sealInternal() error {
// Enable that we are sealed to prevent furthur transactions
c.sealed = true

View File

@ -348,6 +348,17 @@ func TestCore_SealUnseal(t *testing.T) {
}
}
// Attempt to shutdown after unseal
func TestCore_Shutdown(t *testing.T) {
c, _, _ := TestCoreUnsealed(t)
if err := c.Shutdown(); err != nil {
t.Fatalf("err: %v", err)
}
if sealed, err := c.Sealed(); err != nil || !sealed {
t.Fatalf("err: %v", err)
}
}
// Attempt to seal bad token
func TestCore_Seal_BadToken(t *testing.T) {
c, _, _ := TestCoreUnsealed(t)
@ -1368,6 +1379,55 @@ func TestCore_RenewSameLease(t *testing.T) {
}
}
// Renew of a token should not create a new lease
func TestCore_RenewToken_SingleRegister(t *testing.T) {
c, _, root := TestCoreUnsealed(t)
// Create a new token
req := &logical.Request{
Operation: logical.WriteOperation,
Path: "auth/token/create",
Data: map[string]interface{}{
"lease": "1h",
},
ClientToken: root,
}
resp, err := c.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
newClient := resp.Auth.ClientToken
// Renew the token
req = logical.TestRequest(t, logical.WriteOperation, "auth/token/renew/"+newClient)
req.ClientToken = newClient
resp, err = c.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
// Revoke using the renew prefix
req = logical.TestRequest(t, logical.WriteOperation, "sys/revoke-prefix/auth/token/renew/")
req.ClientToken = root
resp, err = c.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
// Verify our token is still valid (e.g. we did not get invalided by the revoke)
req = logical.TestRequest(t, logical.ReadOperation, "auth/token/lookup/"+newClient)
req.ClientToken = newClient
resp, err = c.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
// Verify the token exists
if resp.Data["id"] != newClient {
t.Fatalf("bad: %#v", resp.Data)
}
}
// Based on bug GH-203, attempt to disable a credential backend with leased secrets
func TestCore_EnableDisableCred_WithLease(t *testing.T) {
// Create a badass credential backend that always logs in as armon

View File

@ -337,7 +337,6 @@ func (m *ExpirationManager) RenewToken(source string, token string,
// Attach the ClientToken
resp.Auth.ClientToken = token
resp.Auth.LeaseIncrement = 0
resp.Auth.LeaseIssue = time.Now().UTC()
// Update the lease entry
le.Auth = resp.Auth
@ -366,9 +365,6 @@ func (m *ExpirationManager) Register(req *logical.Request, resp *logical.Respons
return "", err
}
// Setup some of the fields on auth
resp.Secret.LeaseIssue = time.Now().UTC()
// Create a lease entry
le := leaseEntry{
LeaseID: path.Join(req.Path, generateUUID()),
@ -376,7 +372,7 @@ func (m *ExpirationManager) Register(req *logical.Request, resp *logical.Respons
Path: req.Path,
Data: resp.Data,
Secret: resp.Secret,
IssueTime: resp.Secret.LeaseIssue,
IssueTime: time.Now().UTC(),
ExpireTime: resp.Secret.ExpirationTime(),
}
@ -403,16 +399,13 @@ func (m *ExpirationManager) Register(req *logical.Request, resp *logical.Respons
func (m *ExpirationManager) RegisterAuth(source string, auth *logical.Auth) error {
defer metrics.MeasureSince([]string{"expire", "register-auth"}, time.Now())
// Setup some of the fields on auth
auth.LeaseIssue = time.Now().UTC()
// Create a lease entry
le := leaseEntry{
LeaseID: path.Join(source, m.tokenStore.SaltID(auth.ClientToken)),
ClientToken: auth.ClientToken,
Auth: auth,
Path: source,
IssueTime: auth.LeaseIssue,
IssueTime: time.Now().UTC(),
ExpireTime: auth.ExpirationTime(),
}
@ -642,7 +635,7 @@ func (l *leaseEntry) encode() ([]byte, error) {
func (le *leaseEntry) renewable() error {
// If there is no entry, cannot review
if le == nil || le.ExpireTime.IsZero() {
return fmt.Errorf("lease not found")
return fmt.Errorf("lease not found or lease is not renewable")
}
// Determine if the lease is expired

View File

@ -152,7 +152,7 @@ func TestExpiration_RegisterAuth_NoLease(t *testing.T) {
// Should not be able to renew, no expiration
_, err = exp.RenewToken("auth/github/login", root.ID, 0)
if err.Error() != "lease not found" {
if err.Error() != "lease not found or lease is not renewable" {
t.Fatalf("err: %v", err)
}

View File

@ -110,7 +110,7 @@ func NewSystemBackend(core *Core) logical.Backend {
Description: strings.TrimSpace(sysHelp["lease_id"][0]),
},
"increment": &framework.FieldSchema{
Type: framework.TypeInt,
Type: framework.TypeDurationSecond,
Description: strings.TrimSpace(sysHelp["increment"][0]),
},
},

View File

@ -181,7 +181,7 @@ func TestSystemBackend_renew(t *testing.T) {
// Attempt renew
req2 := logical.TestRequest(t, logical.WriteOperation, "renew/"+resp.Secret.LeaseID)
req2.Data["increment"] = 100
req2.Data["increment"] = "100s"
resp2, err := b.HandleRequest(req2)
if err != logical.ErrInvalidRequest {
t.Fatalf("err: %v", err)
@ -202,7 +202,7 @@ func TestSystemBackend_renew_invalidID(t *testing.T) {
if err != logical.ErrInvalidRequest {
t.Fatalf("err: %v", err)
}
if resp.Data["error"] != "lease not found" {
if resp.Data["error"] != "lease not found or lease is not renewable" {
t.Fatalf("bad: %v", resp)
}
}
@ -250,7 +250,7 @@ func TestSystemBackend_revoke(t *testing.T) {
if err != logical.ErrInvalidRequest {
t.Fatalf("err: %v", err)
}
if resp3.Data["error"] != "lease not found" {
if resp3.Data["error"] != "lease not found or lease is not renewable" {
t.Fatalf("bad: %v", resp)
}
}
@ -312,7 +312,7 @@ func TestSystemBackend_revokePrefix(t *testing.T) {
if err != logical.ErrInvalidRequest {
t.Fatalf("err: %v", err)
}
if resp3.Data["error"] != "lease not found" {
if resp3.Data["error"] != "lease not found or lease is not renewable" {
t.Fatalf("bad: %v", resp)
}
}

View File

@ -85,7 +85,10 @@ func NewTokenStore(c *Core) (*TokenStore, error) {
// Setup the framework endpoints
t.Backend = &framework.Backend{
AuthRenew: framework.LeaseExtend(0, 0),
// Allow a token lease to be extended indefinitely, but each time for only
// as much as the original lease allowed for. If the lease has a 1 hour expiration,
// it can only be extended up to another hour each time this means.
AuthRenew: framework.LeaseExtend(0, 0, true),
PathsSpecial: &logical.Paths{
Root: []string{
@ -208,7 +211,7 @@ func NewTokenStore(c *Core) (*TokenStore, error) {
Description: "Token to renew",
},
"increment": &framework.FieldSchema{
Type: framework.TypeInt,
Type: framework.TypeDurationSecond,
Description: "The desired increment in seconds to the token expiration",
},
},

View File

@ -820,8 +820,9 @@ func TestTokenStore_HandleRequest_Renew(t *testing.T) {
// Get the original expire time to compare
originalExpire := auth.ExpirationTime()
beforeRenew := time.Now().UTC()
req := logical.TestRequest(t, logical.WriteOperation, "renew/"+root.ID)
req.Data["increment"] = "3600"
req.Data["increment"] = "3600s"
resp, err := ts.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v %v", err, resp)
@ -829,9 +830,11 @@ func TestTokenStore_HandleRequest_Renew(t *testing.T) {
// Get the new expire time
newExpire := resp.Auth.ExpirationTime()
expireDiff := newExpire.Sub(originalExpire)
if expireDiff < 30*time.Minute || expireDiff > 3*time.Hour {
t.Fatalf("bad: %#v", expireDiff)
if newExpire.Before(originalExpire) {
t.Fatalf("should expire later: %s %s", newExpire, originalExpire)
}
if newExpire.Before(beforeRenew.Add(time.Hour)) {
t.Fatalf("should have at least an hour: %s %s", newExpire, beforeRenew)
}
}

View File

@ -76,6 +76,8 @@ durability, etc.
* `s3` - Store data within an S3 bucket [S3](http://aws.amazon.com/s3/).
This backend does not support HA.
* `mysql` - Store data within MySQL. This backend does not support HA.
* `inmem` - Store data in-memory. This is only really useful for
development and experimentation. Data is lost whenever Vault is
restarted.
@ -143,6 +145,21 @@ For S3, the following options are supported:
* `region` (optional) - The AWS region. It can be sourced from the AWS_DEFAULT_REGION environment variable and will default to "us-east-1" if not specified.
#### Backend Reference: MySQL
The MySQL backend has the following options:
* `username` (required) - The MySQL username to connect with.
* `password` (required) - The MySQL password to connect with.
* `address` (optional) - The address of the MySQL host. Defaults to
"127.0.0.1:3306.
* `database` (optional) - The name of the database to use. Defaults to "vault".
* `table` (optional) - The name of the table to use. Defaults to "vault".
#### Backend Reference: Inmem
The in-memory backend has no configuration options.

View File

@ -54,6 +54,15 @@ $ vault read transit/keys/foo
Key Value
name foo
cipher_mode aes-gcm
````
We can read from the `raw/` endpoint to see the encryption key itself:
```
$ vault read transit/raw/foo
Key Value
name foo
cipher_mode aes-gcm
key PhKFTALCmhAhVQfMBAH4+UwJ6J2gybapUH9BsrtIgR8=
````
@ -114,17 +123,7 @@ only encrypt or decrypt using the named keys they need access to.
<dt>Returns</dt>
<dd>
```javascript
{
"data": {
"name": "foo",
"cipher_mode": "aes-gcm",
"key": "PhKFTALCmhAhVQfMBAH4+UwJ6J2gybapUH9BsrtIgR8="
}
}
```
A `204` response code.
</dd>
</dl>
@ -156,7 +155,6 @@ only encrypt or decrypt using the named keys they need access to.
"data": {
"name": "foo",
"cipher_mode": "aes-gcm",
"key": "PhKFTALCmhAhVQfMBAH4+UwJ6J2gybapUH9BsrtIgR8="
}
}
```
@ -196,7 +194,9 @@ only encrypt or decrypt using the named keys they need access to.
<dl class="api">
<dt>Description</dt>
<dd>
Encrypts the provided plaintext using the named key.
Encrypts the provided plaintext using the named key. If the named key
does not already exist, it will be automatically generated for the given
name with the default parameters.
</dd>
<dt>Method</dt>
@ -269,3 +269,42 @@ only encrypt or decrypt using the named keys they need access to.
</dd>
</dl>
### /transit/raw/
#### GET
<dl class="api">
<dt>Description</dt>
<dd>
Returns raw information about a named encryption key,
Including the underlying encryption key. This is a root protected endpoint.
</dd>
<dt>Method</dt>
<dd>GET</dd>
<dt>URL</dt>
<dd>`/transit/raw/<name>`</dd>
<dt>Parameters</dt>
<dd>
None
</dd>
<dt>Returns</dt>
<dd>
```javascript
{
"data": {
"name": "foo",
"cipher_mode": "aes-gcm",
"key": "PhKFTALCmhAhVQfMBAH4+UwJ6J2gybapUH9BsrtIgR8="
}
}
```
</dd>
</dl>