mirror of
https://github.com/hashicorp/vault.git
synced 2026-05-28 04:10:44 -04:00
prototpe key animation
This commit is contained in:
commit
005058374e
63 changed files with 1950 additions and 441 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -29,3 +29,4 @@ pkg/
|
|||
|
||||
# Vault-specific
|
||||
example.hcl
|
||||
example.vault.d
|
||||
|
|
|
|||
37
api/logical.go
Normal file
37
api/logical.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package api
|
||||
|
||||
// Logical is used to perform logical backend operations on Vault.
|
||||
type Logical struct {
|
||||
c *Client
|
||||
}
|
||||
|
||||
// Logical is used to return the client for logical-backend API calls.
|
||||
func (c *Client) Logical() *Logical {
|
||||
return &Logical{c: c}
|
||||
}
|
||||
|
||||
func (c *Logical) Read(path string) (*Secret, error) {
|
||||
r := c.c.NewRequest("GET", "/v1/"+path)
|
||||
resp, err := c.c.RawRequest(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return ParseSecret(resp.Body)
|
||||
}
|
||||
|
||||
func (c *Logical) Write(path string, data map[string]interface{}) error {
|
||||
r := c.c.NewRequest("PUT", "/v1/"+path)
|
||||
if err := r.SetJSONBody(data); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
resp, err := c.c.RawRequest(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
@ -45,13 +45,20 @@ func (r *Response) Error() error {
|
|||
if err := dec.Decode(&resp); err != nil {
|
||||
// Ignore the decoding error and just drop the raw response
|
||||
return fmt.Errorf(
|
||||
"Error making API request. Code: %d. Raw Message:\n\n%s",
|
||||
"Error making API request.\n\n"+
|
||||
"URL: %s %s\n"+
|
||||
"Code: %d. Raw Message:\n\n%s",
|
||||
r.Request.Method, r.Request.URL.String(),
|
||||
r.StatusCode, bodyBuf.String())
|
||||
}
|
||||
|
||||
var errBody bytes.Buffer
|
||||
errBody.WriteString(fmt.Sprintf(
|
||||
"Error making API request. Code: %d. Errors:\n\n", r.StatusCode))
|
||||
"Error making API request.\n\n"+
|
||||
"URL: %s %s\n"+
|
||||
"Code: %d. Errors:\n\n",
|
||||
r.Request.Method, r.Request.URL.String(),
|
||||
r.StatusCode))
|
||||
for _, err := range resp.Errors {
|
||||
errBody.WriteString(fmt.Sprintf("* %s", err))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,49 +3,25 @@ package api
|
|||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
|
||||
"github.com/mitchellh/mapstructure"
|
||||
)
|
||||
|
||||
// Secret is the structure returned for every secret within Vault.
|
||||
type Secret struct {
|
||||
VaultId string `mapstructure:"vault_id"`
|
||||
VaultId string `json:"vault_id"`
|
||||
Renewable bool
|
||||
LeaseDuration int `mapstructure:"lease_duration"`
|
||||
LeaseDurationMax int `mapstructure:"lease_duration_max"`
|
||||
Data map[string]interface{} `mapstructure:"-"`
|
||||
LeaseDuration int `json:"lease_duration"`
|
||||
LeaseDurationMax int `json:"lease_duration_max"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
// ParseSecret is used to parse a secret value from JSON from an io.Reader.
|
||||
func ParseSecret(r io.Reader) (*Secret, error) {
|
||||
// First decode the JSON into a map[string]interface{}
|
||||
var raw map[string]interface{}
|
||||
var secret Secret
|
||||
dec := json.NewDecoder(r)
|
||||
if err := dec.Decode(&raw); err != nil {
|
||||
if err := dec.Decode(&secret); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Use mapstructure to get as much as possible
|
||||
var result Secret
|
||||
var metadata mapstructure.Metadata
|
||||
mdec, err := mapstructure.NewDecoder(&mapstructure.DecoderConfig{
|
||||
Metadata: &metadata,
|
||||
Result: &result,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mdec.Decode(raw); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Delete the keys we decoded from the raw value, then set that
|
||||
// raw value as the resulting data.
|
||||
for _, k := range metadata.Keys {
|
||||
delete(raw, strings.ToLower(k))
|
||||
}
|
||||
|
||||
result.Data = raw
|
||||
return &result, nil
|
||||
return &secret, nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,12 +7,16 @@ import (
|
|||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
// EnvVaultAddress can be used to set the address of Vault
|
||||
const EnvVaultAddress = "VAULT_ADDR"
|
||||
|
||||
// FlagSetFlags is an enum to define what flags are present in the
|
||||
// default FlagSet returned by Meta.FlagSet.
|
||||
type FlagSetFlags uint
|
||||
|
|
@ -39,6 +43,9 @@ type Meta struct {
|
|||
// flag settings for this command.
|
||||
func (m *Meta) Client() (*api.Client, error) {
|
||||
config := api.DefaultConfig()
|
||||
if v := os.Getenv(EnvVaultAddress); v != "" {
|
||||
config.Address = v
|
||||
}
|
||||
if m.flagAddress != "" {
|
||||
config.Address = m.flagAddress
|
||||
}
|
||||
|
|
|
|||
92
command/mounts.go
Normal file
92
command/mounts.go
Normal file
|
|
@ -0,0 +1,92 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// MountsCommand is a Command that lists the mounts.
|
||||
type MountsCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *MountsCommand) Run(args []string) int {
|
||||
flags := c.Meta.FlagSet("mounts", 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)
|
||||
|
||||
for _, path := range paths {
|
||||
mount := mounts[path]
|
||||
|
||||
var desc bytes.Buffer
|
||||
s := bufio.NewScanner(strings.NewReader(mount.Description))
|
||||
for s.Scan() {
|
||||
desc.WriteString(" " + s.Text())
|
||||
}
|
||||
|
||||
c.Ui.Output(fmt.Sprintf(
|
||||
"%s (type: %s)\n%s\n",
|
||||
path,
|
||||
mount.Type,
|
||||
desc.String()))
|
||||
}
|
||||
|
||||
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, and
|
||||
a human-friendly description of the mount point.
|
||||
|
||||
General Options:
|
||||
|
||||
-address=TODO The address of the Vault server.
|
||||
|
||||
-ca-cert=path Path to a PEM encoded CA cert file to use to
|
||||
verify the Vault server SSL certificate.
|
||||
|
||||
-ca-path=path Path to a directory of PEM encoded CA cert files
|
||||
to verify the Vault server SSL certificate. If both
|
||||
-ca-cert and -ca-path are specified, -ca-path is used.
|
||||
|
||||
-insecure Do not verify TLS certificate. This is highly
|
||||
not recommended. This is especially not recommended
|
||||
for unsealing a vault.
|
||||
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
29
command/mounts_test.go
Normal file
29
command/mounts_test.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/http"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestMounts(t *testing.T) {
|
||||
core, _ := vault.TestCoreUnsealed(t)
|
||||
ln, addr := http.TestServer(t, core)
|
||||
defer ln.Close()
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
c := &MountsCommand{
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-address", addr,
|
||||
}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
}
|
||||
}
|
||||
|
|
@ -1,59 +0,0 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// PutCommand is a Command that puts data into the Vault.
|
||||
type PutCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *PutCommand) Run(args []string) int {
|
||||
flags := c.Meta.FlagSet("put", FlagSetDefault)
|
||||
flags.Usage = func() { c.Ui.Error(c.Help()) }
|
||||
if err := flags.Parse(args); err != nil {
|
||||
return 1
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *PutCommand) Synopsis() string {
|
||||
return "Put secrets or configuration into Vault"
|
||||
}
|
||||
|
||||
func (c *PutCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: vault put [options] path data
|
||||
|
||||
Write data (secrets or configuration) into Vault.
|
||||
|
||||
Put sends data into Vault at the given path. The behavior of the write
|
||||
is determined by the backend at the given path. For example, writing
|
||||
to "aws/policy/ops" will create an "ops" IAM policy for the AWS backend
|
||||
(configuration), but writing to "consul/foo" will write a value directly
|
||||
into Consul at that key. Check the documentation of the logical backend
|
||||
you're using for more information on key structure.
|
||||
|
||||
If data is "-" then the data will be ready from stdin. To write a literal
|
||||
"-", you'll have to pipe that value in from stdin.
|
||||
|
||||
General Options:
|
||||
|
||||
-address=TODO The address of the Vault server.
|
||||
|
||||
-ca-cert=path Path to a PEM encoded CA cert file to use to
|
||||
verify the Vault server SSL certificate.
|
||||
|
||||
-ca-path=path Path to a directory of PEM encoded CA cert files
|
||||
to verify the Vault server SSL certificate. If both
|
||||
-ca-cert and -ca-path are specified, -ca-path is used.
|
||||
|
||||
-insecure Do not verify TLS certificate. This is highly
|
||||
not recommended. This is especially not recommended
|
||||
for unsealing a vault.
|
||||
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
|
|
@ -1,29 +1,64 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GetCommand is a Command that gets data from the Vault.
|
||||
type GetCommand struct {
|
||||
// ReadCommand is a Command that gets data from the Vault.
|
||||
type ReadCommand struct {
|
||||
Meta
|
||||
}
|
||||
|
||||
func (c *GetCommand) Run(args []string) int {
|
||||
flags := c.Meta.FlagSet("put", FlagSetDefault)
|
||||
func (c *ReadCommand) Run(args []string) int {
|
||||
flags := c.Meta.FlagSet("read", 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("read 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
|
||||
}
|
||||
|
||||
secret, err := client.Logical().Read(path)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf(
|
||||
"Error reading %s: %s", path, err))
|
||||
return 1
|
||||
}
|
||||
|
||||
b, err := json.Marshal(secret)
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf(
|
||||
"Error reading %s: %s", path, err))
|
||||
return 1
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
json.Indent(&out, b, "", "\t")
|
||||
c.Ui.Output(out.String())
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *GetCommand) Synopsis() string {
|
||||
return "Get data or secrets from Vault"
|
||||
func (c *ReadCommand) Synopsis() string {
|
||||
return "Read data or secrets from Vault"
|
||||
}
|
||||
|
||||
func (c *GetCommand) Help() string {
|
||||
func (c *ReadCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: vault get [options] path
|
||||
|
||||
30
command/read_test.go
Normal file
30
command/read_test.go
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/http"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestRead(t *testing.T) {
|
||||
core, _ := vault.TestCoreUnsealed(t)
|
||||
ln, addr := http.TestServer(t, core)
|
||||
defer ln.Close()
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
c := &ReadCommand{
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-address", addr,
|
||||
"sys/mounts",
|
||||
}
|
||||
if code := c.Run(args); code != 0 {
|
||||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
}
|
||||
}
|
||||
|
|
@ -17,7 +17,7 @@ func TestSealStatus(t *testing.T) {
|
|||
}
|
||||
|
||||
core := vault.TestCore(t)
|
||||
keys := vault.TestCoreInit(t, core)
|
||||
key := vault.TestCoreInit(t, core)
|
||||
ln, addr := http.TestServer(t, core)
|
||||
defer ln.Close()
|
||||
|
||||
|
|
@ -26,10 +26,8 @@ func TestSealStatus(t *testing.T) {
|
|||
t.Fatalf("bad: %d\n\n%s", code, ui.ErrorWriter.String())
|
||||
}
|
||||
|
||||
for _, k := range keys {
|
||||
if _, err := core.Unseal(k); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
if _, err := core.Unseal(key); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if code := c.Run(args); code != 0 {
|
||||
|
|
|
|||
|
|
@ -11,13 +11,13 @@ import (
|
|||
|
||||
func TestUnseal(t *testing.T) {
|
||||
core := vault.TestCore(t)
|
||||
keys := vault.TestCoreInit(t, core)
|
||||
key := vault.TestCoreInit(t, core)
|
||||
ln, addr := http.TestServer(t, core)
|
||||
defer ln.Close()
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
c := &UnsealCommand{
|
||||
Key: hex.EncodeToString(keys[0]),
|
||||
Key: hex.EncodeToString(key),
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
},
|
||||
|
|
|
|||
114
command/write.go
Normal file
114
command/write.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// DefaultDataKey is the key used in the write as a default for data.
|
||||
const DefaultDataKey = "value"
|
||||
|
||||
// WriteCommand is a Command that puts data into the Vault.
|
||||
type WriteCommand struct {
|
||||
Meta
|
||||
|
||||
// The fields below can be overwritten for tests
|
||||
testStdin io.Reader
|
||||
}
|
||||
|
||||
func (c *WriteCommand) Run(args []string) int {
|
||||
flags := c.Meta.FlagSet("write", 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 {
|
||||
c.Ui.Error("write expects two arguments")
|
||||
flags.Usage()
|
||||
return 1
|
||||
}
|
||||
|
||||
path := args[0]
|
||||
if path[0] == '/' {
|
||||
path = path[1:]
|
||||
}
|
||||
var data map[string]interface{}
|
||||
if args[1] == "-" {
|
||||
var stdin io.Reader = os.Stdin
|
||||
if c.testStdin != nil {
|
||||
stdin = c.testStdin
|
||||
}
|
||||
|
||||
dec := json.NewDecoder(stdin)
|
||||
if err := dec.Decode(&data); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf(
|
||||
"Error decoding JSON of stdin: %s", err))
|
||||
return 1
|
||||
}
|
||||
} else {
|
||||
data = map[string]interface{}{DefaultDataKey: args[1]}
|
||||
}
|
||||
|
||||
client, err := c.Client()
|
||||
if err != nil {
|
||||
c.Ui.Error(fmt.Sprintf(
|
||||
"Error initializing client: %s", err))
|
||||
return 2
|
||||
}
|
||||
|
||||
if err := client.Logical().Write(path, data); err != nil {
|
||||
c.Ui.Error(fmt.Sprintf(
|
||||
"Error writing data to %s: %s", path, err))
|
||||
return 1
|
||||
}
|
||||
|
||||
c.Ui.Output(fmt.Sprintf("Success! Data written to: %s", path))
|
||||
return 0
|
||||
}
|
||||
|
||||
func (c *WriteCommand) Synopsis() string {
|
||||
return "Write secrets or configuration into Vault"
|
||||
}
|
||||
|
||||
func (c *WriteCommand) Help() string {
|
||||
helpText := `
|
||||
Usage: vault write [options] path data
|
||||
|
||||
Write data (secrets or configuration) into Vault.
|
||||
|
||||
Write sends data into Vault at the given path. The behavior of the write
|
||||
is determined by the backend at the given path. For example, writing
|
||||
to "aws/policy/ops" will create an "ops" IAM policy for the AWS backend
|
||||
(configuration), but writing to "consul/foo" will write a value directly
|
||||
into Consul at that key. Check the documentation of the logical backend
|
||||
you're using for more information on key structure.
|
||||
|
||||
If data is "-" then the data will be ready from stdin. To write a literal
|
||||
"-", you'll have to pipe that value in from stdin. To write data from a
|
||||
file, pipe the file contents in via stdin and set data to "-".
|
||||
|
||||
If data is a string, it will be sent with the key of "value".
|
||||
|
||||
General Options:
|
||||
|
||||
-address=TODO The address of the Vault server.
|
||||
|
||||
-ca-cert=path Path to a PEM encoded CA cert file to use to
|
||||
verify the Vault server SSL certificate.
|
||||
|
||||
-ca-path=path Path to a directory of PEM encoded CA cert files
|
||||
to verify the Vault server SSL certificate. If both
|
||||
-ca-cert and -ca-path are specified, -ca-path is used.
|
||||
|
||||
-insecure Do not verify TLS certificate. This is highly
|
||||
not recommended. This is especially not recommended
|
||||
for unsealing a vault.
|
||||
|
||||
`
|
||||
return strings.TrimSpace(helpText)
|
||||
}
|
||||
90
command/write_test.go
Normal file
90
command/write_test.go
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
package command
|
||||
|
||||
import (
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/http"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
"github.com/mitchellh/cli"
|
||||
)
|
||||
|
||||
func TestWrite(t *testing.T) {
|
||||
core, _ := vault.TestCoreUnsealed(t)
|
||||
ln, addr := http.TestServer(t, core)
|
||||
defer ln.Close()
|
||||
|
||||
ui := new(cli.MockUi)
|
||||
c := &WriteCommand{
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
},
|
||||
}
|
||||
|
||||
args := []string{
|
||||
"-address", addr,
|
||||
"secret/foo",
|
||||
"bar",
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
resp, err := client.Logical().Read("secret/foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if resp.Data[DefaultDataKey] != "bar" {
|
||||
t.Fatalf("bad: %#v", resp)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWrite_arbitrary(t *testing.T) {
|
||||
core, _ := vault.TestCoreUnsealed(t)
|
||||
ln, addr := http.TestServer(t, core)
|
||||
defer ln.Close()
|
||||
|
||||
stdinR, stdinW := io.Pipe()
|
||||
ui := new(cli.MockUi)
|
||||
c := &WriteCommand{
|
||||
Meta: Meta{
|
||||
Ui: ui,
|
||||
},
|
||||
|
||||
testStdin: stdinR,
|
||||
}
|
||||
|
||||
go func() {
|
||||
stdinW.Write([]byte(`{"foo":"bar"}`))
|
||||
stdinW.Close()
|
||||
}()
|
||||
|
||||
args := []string{
|
||||
"-address", addr,
|
||||
"secret/foo",
|
||||
"-",
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
resp, err := client.Logical().Read("secret/foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
if resp.Data["foo"] != "bar" {
|
||||
t.Fatalf("bad: %#v", resp)
|
||||
}
|
||||
}
|
||||
14
commands.go
14
commands.go
|
|
@ -24,14 +24,14 @@ func init() {
|
|||
}, nil
|
||||
},
|
||||
|
||||
"get": func() (cli.Command, error) {
|
||||
return &command.GetCommand{
|
||||
"read": func() (cli.Command, error) {
|
||||
return &command.ReadCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"put": func() (cli.Command, error) {
|
||||
return &command.PutCommand{
|
||||
"write": func() (cli.Command, error) {
|
||||
return &command.WriteCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
|
@ -66,6 +66,12 @@ func init() {
|
|||
}, nil
|
||||
},
|
||||
|
||||
"mounts": func() (cli.Command, error) {
|
||||
return &command.MountsCommand{
|
||||
Meta: meta,
|
||||
}, nil
|
||||
},
|
||||
|
||||
"version": func() (cli.Command, error) {
|
||||
ver := Version
|
||||
rel := VersionPrerelease
|
||||
|
|
|
|||
|
|
@ -15,6 +15,8 @@ func Handler(core *vault.Core) http.Handler {
|
|||
mux.Handle("/v1/sys/seal-status", handleSysSealStatus(core))
|
||||
mux.Handle("/v1/sys/seal", handleSysSeal(core))
|
||||
mux.Handle("/v1/sys/unseal", handleSysUnseal(core))
|
||||
mux.Handle("/v1/sys/mounts/", handleSysMounts(core))
|
||||
mux.Handle("/v1/", handleLogical(core))
|
||||
return mux
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,19 @@ import (
|
|||
"testing"
|
||||
)
|
||||
|
||||
func testHttpDelete(t *testing.T, addr string) *http.Response {
|
||||
return testHttpData(t, "DELETE", addr, nil)
|
||||
}
|
||||
|
||||
func testHttpPost(t *testing.T, addr string, body interface{}) *http.Response {
|
||||
return testHttpData(t, "POST", addr, body)
|
||||
}
|
||||
|
||||
func testHttpPut(t *testing.T, addr string, body interface{}) *http.Response {
|
||||
return testHttpData(t, "PUT", addr, body)
|
||||
}
|
||||
|
||||
func testHttpData(t *testing.T, method string, addr string, body interface{}) *http.Response {
|
||||
bodyReader := new(bytes.Buffer)
|
||||
if body != nil {
|
||||
enc := json.NewEncoder(bodyReader)
|
||||
|
|
@ -17,7 +29,7 @@ func testHttpPut(t *testing.T, addr string, body interface{}) *http.Response {
|
|||
}
|
||||
}
|
||||
|
||||
req, err := http.NewRequest("PUT", addr, bodyReader)
|
||||
req, err := http.NewRequest(method, addr, bodyReader)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
|
|
|||
84
http/logical.go
Normal file
84
http/logical.go
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
)
|
||||
|
||||
func handleLogical(core *vault.Core) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Determine the path...
|
||||
if !strings.HasPrefix(r.URL.Path, "/v1/") {
|
||||
respondError(w, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
path := r.URL.Path[len("/v1/"):]
|
||||
if path == "" {
|
||||
respondError(w, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Determine the operation
|
||||
var op logical.Operation
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
op = logical.ReadOperation
|
||||
case "PUT":
|
||||
op = logical.WriteOperation
|
||||
default:
|
||||
respondError(w, http.StatusMethodNotAllowed, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Parse the request if we can
|
||||
var req map[string]interface{}
|
||||
if op == logical.WriteOperation {
|
||||
if err := parseRequest(r, &req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Make the internal request
|
||||
resp, err := core.HandleRequest(&logical.Request{
|
||||
Operation: op,
|
||||
Path: path,
|
||||
Data: req,
|
||||
})
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
if op == logical.ReadOperation && resp == nil {
|
||||
respondError(w, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
var httpResp interface{}
|
||||
if resp != nil {
|
||||
logicalResp := &LogicalResponse{Data: resp.Data}
|
||||
if resp.IsSecret && resp.Lease != nil {
|
||||
logicalResp.VaultId = resp.Lease.VaultID
|
||||
logicalResp.Renewable = resp.Lease.Renewable
|
||||
logicalResp.LeaseDuration = int(resp.Lease.Duration.Seconds())
|
||||
logicalResp.LeaseDurationMax = int(resp.Lease.MaxDuration.Seconds())
|
||||
}
|
||||
|
||||
httpResp = logicalResp
|
||||
}
|
||||
|
||||
// Respond
|
||||
respondOk(w, httpResp)
|
||||
})
|
||||
}
|
||||
|
||||
type LogicalResponse struct {
|
||||
VaultId string `json:"vault_id"`
|
||||
Renewable bool `json:"renewable"`
|
||||
LeaseDuration int `json:"lease_duration"`
|
||||
LeaseDurationMax int `json:"lease_duration_max"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
53
http/logical_test.go
Normal file
53
http/logical_test.go
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/vault"
|
||||
)
|
||||
|
||||
func TestLogical(t *testing.T) {
|
||||
core, _ := vault.TestCoreUnsealed(t)
|
||||
ln, addr := TestServer(t, core)
|
||||
defer ln.Close()
|
||||
|
||||
resp := testHttpPut(t, addr+"/v1/secret/foo", map[string]interface{}{
|
||||
"data": "bar",
|
||||
})
|
||||
testResponseStatus(t, resp, 204)
|
||||
|
||||
resp, err := http.Get(addr + "/v1/secret/foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
var actual map[string]interface{}
|
||||
expected := map[string]interface{}{
|
||||
"vault_id": "",
|
||||
"renewable": false,
|
||||
"lease_duration": float64(0),
|
||||
"lease_duration_max": float64(0),
|
||||
"data": map[string]interface{}{
|
||||
"data": "bar",
|
||||
},
|
||||
}
|
||||
testResponseStatus(t, resp, 200)
|
||||
testResponseBody(t, resp, &actual)
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Fatalf("bad: %#v", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLogical_noExist(t *testing.T) {
|
||||
core, _ := vault.TestCoreUnsealed(t)
|
||||
ln, addr := TestServer(t, core)
|
||||
defer ln.Close()
|
||||
|
||||
resp, err := http.Get(addr + "/v1/secret/foo")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
testResponseStatus(t, resp, 404)
|
||||
}
|
||||
124
http/sys_mount.go
Normal file
124
http/sys_mount.go
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
)
|
||||
|
||||
func handleSysMounts(core *vault.Core) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "GET":
|
||||
handleSysListMounts(core, w, r)
|
||||
case "POST":
|
||||
fallthrough
|
||||
case "DELETE":
|
||||
handleSysMountUnmount(core, w, r)
|
||||
default:
|
||||
respondError(w, http.StatusMethodNotAllowed, nil)
|
||||
return
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func handleSysListMounts(core *vault.Core, w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != "GET" {
|
||||
respondError(w, http.StatusMethodNotAllowed, nil)
|
||||
return
|
||||
}
|
||||
|
||||
resp, err := core.HandleRequest(&logical.Request{
|
||||
Operation: logical.ReadOperation,
|
||||
Path: "sys/mounts",
|
||||
})
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
respondOk(w, resp.Data)
|
||||
}
|
||||
|
||||
func handleSysMountUnmount(core *vault.Core, w http.ResponseWriter, r *http.Request) {
|
||||
switch r.Method {
|
||||
case "POST":
|
||||
case "DELETE":
|
||||
default:
|
||||
respondError(w, http.StatusMethodNotAllowed, nil)
|
||||
return
|
||||
}
|
||||
|
||||
// Determine the path...
|
||||
prefix := "/v1/sys/mounts/"
|
||||
if !strings.HasPrefix(r.URL.Path, prefix) {
|
||||
respondError(w, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
path := r.URL.Path[len(prefix):]
|
||||
if path == "" {
|
||||
respondError(w, http.StatusNotFound, nil)
|
||||
return
|
||||
}
|
||||
|
||||
switch r.Method {
|
||||
case "POST":
|
||||
handleSysMount(core, w, r, path)
|
||||
case "DELETE":
|
||||
handleSysUnmount(core, w, r, path)
|
||||
default:
|
||||
panic("should never happen")
|
||||
}
|
||||
}
|
||||
|
||||
func handleSysMount(
|
||||
core *vault.Core,
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
path string) {
|
||||
// Parse the request if we can
|
||||
var req MountRequest
|
||||
if err := parseRequest(r, &req); err != nil {
|
||||
respondError(w, http.StatusBadRequest, err)
|
||||
return
|
||||
}
|
||||
|
||||
_, err := core.HandleRequest(&logical.Request{
|
||||
Operation: logical.WriteOperation,
|
||||
Path: "sys/mounts/" + path,
|
||||
Data: map[string]interface{}{
|
||||
"type": req.Type,
|
||||
"description": req.Description,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
respondOk(w, nil)
|
||||
}
|
||||
|
||||
func handleSysUnmount(
|
||||
core *vault.Core,
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
path string) {
|
||||
_, err := core.HandleRequest(&logical.Request{
|
||||
Operation: logical.DeleteOperation,
|
||||
Path: "sys/mounts/" + path,
|
||||
})
|
||||
if err != nil {
|
||||
respondError(w, http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
|
||||
respondOk(w, nil)
|
||||
}
|
||||
|
||||
type MountRequest struct {
|
||||
Type string `json:"type"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
112
http/sys_mount_test.go
Normal file
112
http/sys_mount_test.go
Normal file
|
|
@ -0,0 +1,112 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/vault"
|
||||
)
|
||||
|
||||
func TestSysMounts(t *testing.T) {
|
||||
core, _ := vault.TestCoreUnsealed(t)
|
||||
ln, addr := TestServer(t, core)
|
||||
defer ln.Close()
|
||||
|
||||
resp, err := http.Get(addr + "/v1/sys/mounts")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
var actual map[string]interface{}
|
||||
expected := map[string]interface{}{
|
||||
"secret/": map[string]interface{}{
|
||||
"description": "generic secret storage",
|
||||
"type": "generic",
|
||||
},
|
||||
"sys/": map[string]interface{}{
|
||||
"description": "system endpoints used for control, policy and debugging",
|
||||
"type": "system",
|
||||
},
|
||||
}
|
||||
testResponseStatus(t, resp, 200)
|
||||
testResponseBody(t, resp, &actual)
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Fatalf("bad: %#v", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSysMount(t *testing.T) {
|
||||
core, _ := vault.TestCoreUnsealed(t)
|
||||
ln, addr := TestServer(t, core)
|
||||
defer ln.Close()
|
||||
|
||||
resp := testHttpPost(t, addr+"/v1/sys/mounts/foo", map[string]interface{}{
|
||||
"type": "generic",
|
||||
"description": "foo",
|
||||
})
|
||||
testResponseStatus(t, resp, 204)
|
||||
|
||||
resp, err := http.Get(addr + "/v1/sys/mounts")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
var actual map[string]interface{}
|
||||
expected := map[string]interface{}{
|
||||
"foo/": map[string]interface{}{
|
||||
"description": "foo",
|
||||
"type": "generic",
|
||||
},
|
||||
"secret/": map[string]interface{}{
|
||||
"description": "generic secret storage",
|
||||
"type": "generic",
|
||||
},
|
||||
"sys/": map[string]interface{}{
|
||||
"description": "system endpoints used for control, policy and debugging",
|
||||
"type": "system",
|
||||
},
|
||||
}
|
||||
testResponseStatus(t, resp, 200)
|
||||
testResponseBody(t, resp, &actual)
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Fatalf("bad: %#v", actual)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSysUnmount(t *testing.T) {
|
||||
core, _ := vault.TestCoreUnsealed(t)
|
||||
ln, addr := TestServer(t, core)
|
||||
defer ln.Close()
|
||||
|
||||
resp := testHttpPost(t, addr+"/v1/sys/mounts/foo", map[string]interface{}{
|
||||
"type": "generic",
|
||||
"description": "foo",
|
||||
})
|
||||
testResponseStatus(t, resp, 204)
|
||||
|
||||
resp = testHttpDelete(t, addr+"/v1/sys/mounts/foo")
|
||||
testResponseStatus(t, resp, 204)
|
||||
|
||||
resp, err := http.Get(addr + "/v1/sys/mounts")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
var actual map[string]interface{}
|
||||
expected := map[string]interface{}{
|
||||
"secret/": map[string]interface{}{
|
||||
"description": "generic secret storage",
|
||||
"type": "generic",
|
||||
},
|
||||
"sys/": map[string]interface{}{
|
||||
"description": "system endpoints used for control, policy and debugging",
|
||||
"type": "system",
|
||||
},
|
||||
}
|
||||
testResponseStatus(t, resp, 200)
|
||||
testResponseBody(t, resp, &actual)
|
||||
if !reflect.DeepEqual(actual, expected) {
|
||||
t.Fatalf("bad: %#v", actual)
|
||||
}
|
||||
}
|
||||
|
|
@ -57,8 +57,8 @@ func TestSysSeal_unsealed(t *testing.T) {
|
|||
ln, addr := TestServer(t, core)
|
||||
defer ln.Close()
|
||||
|
||||
keys := vault.TestCoreInit(t, core)
|
||||
if _, err := core.Unseal(keys[0]); err != nil {
|
||||
key := vault.TestCoreInit(t, core)
|
||||
if _, err := core.Unseal(key); err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
|
|
@ -76,12 +76,12 @@ func TestSysSeal_unsealed(t *testing.T) {
|
|||
|
||||
func TestSysUnseal(t *testing.T) {
|
||||
core := vault.TestCore(t)
|
||||
keys := vault.TestCoreInit(t, core)
|
||||
key := vault.TestCoreInit(t, core)
|
||||
ln, addr := TestServer(t, core)
|
||||
defer ln.Close()
|
||||
|
||||
resp := testHttpPut(t, addr+"/v1/sys/unseal", map[string]interface{}{
|
||||
"key": hex.EncodeToString(keys[0]),
|
||||
"key": hex.EncodeToString(key),
|
||||
})
|
||||
|
||||
var actual map[string]interface{}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"testing"
|
||||
|
|
@ -9,9 +10,16 @@ import (
|
|||
)
|
||||
|
||||
func TestServer(t *testing.T, core *vault.Core) (net.Listener, string) {
|
||||
fail := func(format string, args ...interface{}) {
|
||||
panic(fmt.Sprintf(format, args...))
|
||||
}
|
||||
if t != nil {
|
||||
fail = t.Fatalf
|
||||
}
|
||||
|
||||
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
fail("err: %s", err)
|
||||
}
|
||||
addr := "http://" + ln.Addr().String()
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
package backend
|
||||
package framework
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
|
|
@ -24,6 +24,12 @@ type Backend struct {
|
|||
// backend is in use).
|
||||
Paths []*Path
|
||||
|
||||
// PathsRoot is the list of path patterns that denote the
|
||||
// paths above that require root-level privileges. These can't be
|
||||
// regular expressions, it is either exact match or prefix match.
|
||||
// For prefix match, append '*' as a suffix.
|
||||
PathsRoot []string
|
||||
|
||||
once sync.Once
|
||||
pathsRe []*regexp.Regexp
|
||||
}
|
||||
|
|
@ -46,12 +52,6 @@ type Path struct {
|
|||
// whereas all fields are avaiable in the Write operation.
|
||||
Fields map[string]*FieldSchema
|
||||
|
||||
// Root if not blank, denotes that this path requires root
|
||||
// privileges and the path pattern that is the root path. This can't
|
||||
// be a regular expression and must be an exact path. It may have a
|
||||
// trailing '*' to denote that it is a prefix, and not an exact match.
|
||||
Root string
|
||||
|
||||
// Callbacks are the set of callbacks that are called for a given
|
||||
// operation. If a callback for a specific operation is not present,
|
||||
// then logical.ErrUnsupportedOperation is automatically generated.
|
||||
|
|
@ -123,8 +123,7 @@ func (b *Backend) HandleRequest(req *logical.Request) (*logical.Response, error)
|
|||
|
||||
// logical.Backend impl.
|
||||
func (b *Backend) RootPaths() []string {
|
||||
// TODO
|
||||
return nil
|
||||
return b.PathsRoot
|
||||
}
|
||||
|
||||
// Route looks up the path that would be used for a given path string.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package backend
|
||||
package framework
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package backend
|
||||
package framework
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package backend
|
||||
package framework
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package backend
|
||||
package framework
|
||||
|
||||
//go:generate stringer -type=FieldType field_type.go
|
||||
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
// generated by stringer -type=FieldType field_type.go; DO NOT EDIT
|
||||
|
||||
package backend
|
||||
package framework
|
||||
|
||||
import "fmt"
|
||||
|
||||
|
|
@ -1,11 +1,16 @@
|
|||
package backend
|
||||
package testing
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/api"
|
||||
"github.com/hashicorp/vault/http"
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/physical"
|
||||
"github.com/hashicorp/vault/vault"
|
||||
)
|
||||
|
||||
// TestEnvVar must be set to a non-empty value for acceptance tests to run.
|
||||
|
|
@ -86,6 +91,82 @@ func Test(t TestT, c TestCase) {
|
|||
if c.PreCheck != nil {
|
||||
c.PreCheck()
|
||||
}
|
||||
|
||||
// Create an in-memory Vault core
|
||||
core, err := vault.NewCore(&vault.CoreConfig{
|
||||
Physical: physical.NewInmem(),
|
||||
Backends: map[string]logical.Factory{
|
||||
"test": func(map[string]string) (logical.Backend, error) {
|
||||
return c.Backend, nil
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal("error initializing core: ", err)
|
||||
}
|
||||
|
||||
// Initialize the core
|
||||
init, err := core.Initialize(&vault.SealConfig{
|
||||
SecretShares: 1,
|
||||
SecretThreshold: 1,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal("error initializing core: ", err)
|
||||
}
|
||||
|
||||
// Unseal the core
|
||||
if sealed, err := core.Unseal(init.SecretShares[0]); err != nil {
|
||||
t.Fatal("error unsealing core: ", err)
|
||||
} else if sealed {
|
||||
t.Fatal("vault shouldn't be sealed")
|
||||
}
|
||||
|
||||
// Create an HTTP API server and client
|
||||
ln, addr := http.TestServer(nil, core)
|
||||
defer ln.Close()
|
||||
clientConfig := api.DefaultConfig()
|
||||
clientConfig.Address = addr
|
||||
client, err := api.NewClient(clientConfig)
|
||||
if err != nil {
|
||||
t.Fatal("error initializing HTTP client: ", err)
|
||||
}
|
||||
|
||||
// Mount the backend
|
||||
prefix := "mnt"
|
||||
if err := client.Sys().Mount(prefix, "test", "acceptance test"); err != nil {
|
||||
t.Fatal("error mounting backend: ", err)
|
||||
}
|
||||
|
||||
// Make requests
|
||||
for i, s := range c.Steps {
|
||||
log.Printf("[WARN] Executing test step %d", i+1)
|
||||
|
||||
// Make sure to prefix the path with where we mounted the thing
|
||||
path := fmt.Sprintf("%s/%s", prefix, s.Path)
|
||||
|
||||
// Create the request
|
||||
req := &logical.Request{
|
||||
Operation: s.Operation,
|
||||
Path: path,
|
||||
Data: s.Data,
|
||||
}
|
||||
|
||||
// Make the request
|
||||
resp, err := core.HandleRequest(req)
|
||||
if err == nil && s.Check != nil {
|
||||
// Call the test method
|
||||
err = s.Check(resp)
|
||||
}
|
||||
if err != nil {
|
||||
t.Error(fmt.Sprintf("Failed step %d: %s", i+1, err))
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup
|
||||
if c.Teardown != nil {
|
||||
c.Teardown()
|
||||
}
|
||||
}
|
||||
|
||||
// TestT is the interface used to handle the test lifecycle of a test.
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
package backend
|
||||
package testing
|
||||
|
||||
import (
|
||||
"os"
|
||||
128
physical/file.go
128
physical/file.go
|
|
@ -1,13 +1,137 @@
|
|||
package physical
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// FileBackend is a physical backend that stores data on disk
|
||||
// at a given file path. It can be used for durable single server
|
||||
// situations, or to develop locally where durability is not critical.
|
||||
//
|
||||
// WARNING: the file backend implementation is currently extremely unsafe
|
||||
// and non-performant. It is meant mostly for local testing and development.
|
||||
// It can be improved in the future.
|
||||
type FileBackend struct {
|
||||
Path string
|
||||
|
||||
l sync.Mutex
|
||||
}
|
||||
|
||||
// newFileBackend constructs a Filebackend using the given directory
|
||||
func newFileBackend(conf map[string]string) (Backend, error) {
|
||||
// TODO:
|
||||
return nil, nil
|
||||
path, ok := conf["path"]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("'path' must be set")
|
||||
}
|
||||
|
||||
return &FileBackend{Path: path}, nil
|
||||
}
|
||||
|
||||
func (b *FileBackend) Delete(k string) error {
|
||||
b.l.Lock()
|
||||
defer b.l.Unlock()
|
||||
|
||||
path, key := b.path(k)
|
||||
path = filepath.Join(path, key)
|
||||
|
||||
err := os.Remove(path)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
err = nil
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func (b *FileBackend) Get(k string) (*Entry, error) {
|
||||
b.l.Lock()
|
||||
defer b.l.Unlock()
|
||||
|
||||
path, key := b.path(k)
|
||||
path = filepath.Join(path, key)
|
||||
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
var entry Entry
|
||||
dec := json.NewDecoder(f)
|
||||
if err := dec.Decode(&entry); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &entry, nil
|
||||
}
|
||||
|
||||
func (b *FileBackend) Put(entry *Entry) error {
|
||||
path, key := b.path(entry.Key)
|
||||
|
||||
b.l.Lock()
|
||||
defer b.l.Unlock()
|
||||
|
||||
// Make the parent tree
|
||||
if err := os.MkdirAll(path, 0755); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// JSON encode the entry and write it
|
||||
f, err := os.Create(filepath.Join(path, key))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer f.Close()
|
||||
enc := json.NewEncoder(f)
|
||||
return enc.Encode(entry)
|
||||
}
|
||||
|
||||
func (b *FileBackend) List(prefix string) ([]string, error) {
|
||||
b.l.Lock()
|
||||
defer b.l.Unlock()
|
||||
|
||||
path := b.Path
|
||||
if prefix != "" {
|
||||
path = filepath.Join(path, prefix)
|
||||
}
|
||||
|
||||
// Read the directory contents
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
return nil, err
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
names, err := f.Readdirnames(-1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
for i, name := range names {
|
||||
if name[0] == '_' {
|
||||
names[i] = name[1:]
|
||||
} else {
|
||||
names[i] = name + "/"
|
||||
}
|
||||
}
|
||||
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func (b *FileBackend) path(k string) (string, string) {
|
||||
path := filepath.Join(b.Path, k)
|
||||
key := filepath.Base(path)
|
||||
path = filepath.Dir(path)
|
||||
return path, "_" + key
|
||||
}
|
||||
|
|
|
|||
25
physical/file_test.go
Normal file
25
physical/file_test.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
package physical
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFileBackend(t *testing.T) {
|
||||
dir, err := ioutil.TempDir("", "vault")
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
|
||||
b, err := NewBackend("file", map[string]string{
|
||||
"path": dir,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
testBackend(t, b)
|
||||
testBackend_ListPrefix(t, b)
|
||||
}
|
||||
|
|
@ -96,6 +96,9 @@ type Core struct {
|
|||
// router is responsible for managing the mount points for logical backends.
|
||||
router *Router
|
||||
|
||||
// backends is the mapping of backends to use for this core
|
||||
backends map[string]logical.Factory
|
||||
|
||||
// stateLock protects mutable state
|
||||
stateLock sync.RWMutex
|
||||
sealed bool
|
||||
|
|
@ -121,6 +124,7 @@ type Core struct {
|
|||
|
||||
// CoreConfig is used to parameterize a core
|
||||
type CoreConfig struct {
|
||||
Backends map[string]logical.Factory
|
||||
Physical physical.Backend
|
||||
Logger *log.Logger
|
||||
}
|
||||
|
|
@ -146,6 +150,18 @@ func NewCore(conf *CoreConfig) (*Core, error) {
|
|||
sealed: true,
|
||||
logger: conf.Logger,
|
||||
}
|
||||
|
||||
// Setup the backends
|
||||
backends := make(map[string]logical.Factory)
|
||||
for k, f := range conf.Backends {
|
||||
backends[k] = f
|
||||
}
|
||||
backends["generic"] = PassthroughBackendFactory
|
||||
backends["system"] = func(map[string]string) (logical.Backend, error) {
|
||||
return NewSystemBackend(c), nil
|
||||
}
|
||||
|
||||
c.backends = backends
|
||||
return c, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,8 @@ package vault
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"sync"
|
||||
"time"
|
||||
|
|
@ -21,19 +23,25 @@ const (
|
|||
// If a secret is not renewed in timely manner, it may be expired, and
|
||||
// the ExpirationManager will handle doing automatic revocation.
|
||||
type ExpirationManager struct {
|
||||
router *Router
|
||||
view *BarrierView
|
||||
doneCh chan struct{}
|
||||
stopCh chan struct{}
|
||||
stopLock sync.Mutex
|
||||
router *Router
|
||||
view *BarrierView
|
||||
logger *log.Logger
|
||||
|
||||
pending map[string]*time.Timer
|
||||
pendingLock sync.RWMutex
|
||||
}
|
||||
|
||||
// NewExpirationManager creates a new ExpirationManager that is backed
|
||||
// using a given view, and uses the provided router for revocation.
|
||||
func NewExpirationManager(router *Router, view *BarrierView) *ExpirationManager {
|
||||
func NewExpirationManager(router *Router, view *BarrierView, logger *log.Logger) *ExpirationManager {
|
||||
if logger == nil {
|
||||
logger = log.New(os.Stderr, "", log.LstdFlags)
|
||||
}
|
||||
exp := &ExpirationManager{
|
||||
router: router,
|
||||
view: view,
|
||||
router: router,
|
||||
view: view,
|
||||
logger: logger,
|
||||
pending: make(map[string]*time.Timer),
|
||||
}
|
||||
return exp
|
||||
}
|
||||
|
|
@ -45,18 +53,13 @@ func (c *Core) setupExpiration() error {
|
|||
view := c.systemView.SubView(expirationSubPath)
|
||||
|
||||
// Create the manager
|
||||
mgr := NewExpirationManager(c.router, view)
|
||||
mgr := NewExpirationManager(c.router, view, c.logger)
|
||||
c.expiration = mgr
|
||||
|
||||
// Restore the existing state
|
||||
if err := c.expiration.Restore(); err != nil {
|
||||
return fmt.Errorf("expiration state restore failed: %v", err)
|
||||
}
|
||||
|
||||
// Start the expiration manager
|
||||
if err := c.expiration.Start(); err != nil {
|
||||
return fmt.Errorf("expiration start failed: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
@ -73,55 +76,23 @@ func (c *Core) stopExpiration() error {
|
|||
// Restore is used to recover the lease states when starting.
|
||||
// This is used after starting the vault.
|
||||
func (m *ExpirationManager) Restore() error {
|
||||
m.stopLock.Lock()
|
||||
defer m.stopLock.Unlock()
|
||||
if m.stopCh != nil {
|
||||
return fmt.Errorf("cannot restore while running")
|
||||
}
|
||||
|
||||
// TODO: Restore...
|
||||
return nil
|
||||
}
|
||||
|
||||
// Start is used to continue automatic revocation. This
|
||||
// should only be called when the Vault is unsealed.
|
||||
func (m *ExpirationManager) Start() error {
|
||||
m.stopLock.Lock()
|
||||
defer m.stopLock.Unlock()
|
||||
if m.stopCh == nil {
|
||||
m.doneCh = make(chan struct{})
|
||||
m.stopCh = make(chan struct{})
|
||||
go m.run(m.doneCh, m.stopCh)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Stop is used to prevent further automatic revocations.
|
||||
// This must be called before sealing the view.
|
||||
func (m *ExpirationManager) Stop() error {
|
||||
m.stopLock.Lock()
|
||||
defer m.stopLock.Unlock()
|
||||
if m.stopCh != nil {
|
||||
doneCh := m.doneCh
|
||||
close(m.stopCh)
|
||||
m.stopCh = nil
|
||||
m.doneCh = nil
|
||||
<-doneCh // Wait for completion
|
||||
// Stop all the pending expiration timers
|
||||
m.pendingLock.Lock()
|
||||
for _, timer := range m.pending {
|
||||
timer.Stop()
|
||||
}
|
||||
m.pending = make(map[string]*time.Timer)
|
||||
m.pendingLock.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
// run is a long running goroutine that manages background expiration
|
||||
func (m *ExpirationManager) run(doneCh, stopCh chan struct{}) {
|
||||
defer close(doneCh)
|
||||
for {
|
||||
select {
|
||||
case <-stopCh:
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Revoke is used to revoke a secret named by the given vaultID
|
||||
func (m *ExpirationManager) Revoke(vaultID string) error {
|
||||
return nil
|
||||
|
|
@ -160,18 +131,91 @@ func (m *ExpirationManager) Register(req *logical.Request, resp *logical.Respons
|
|||
}
|
||||
|
||||
// Create a lease entry
|
||||
now := time.Now().UTC()
|
||||
le := leaseEntry{
|
||||
VaultID: path.Join(req.Path, generateUUID()),
|
||||
Path: req.Path,
|
||||
Data: resp.Data,
|
||||
Lease: resp.Lease,
|
||||
IssueTime: time.Now().UTC(),
|
||||
IssueTime: now,
|
||||
RenewTime: now,
|
||||
}
|
||||
|
||||
// Encode the entry
|
||||
if err := m.persistEntry(&le); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Setup revocation timer
|
||||
m.pendingLock.Lock()
|
||||
timer := time.AfterFunc(resp.Lease.Duration, func() {
|
||||
m.expireID(le.VaultID)
|
||||
})
|
||||
m.pending[le.VaultID] = timer
|
||||
m.pendingLock.Unlock()
|
||||
|
||||
// Done
|
||||
return le.VaultID, nil
|
||||
}
|
||||
|
||||
// expireID is invoked when a given ID is expired
|
||||
func (m *ExpirationManager) expireID(vaultID string) {
|
||||
// Clear from the pending expiration
|
||||
m.pendingLock.Lock()
|
||||
delete(m.pending, vaultID)
|
||||
m.pendingLock.Unlock()
|
||||
|
||||
// Load the entry
|
||||
le, err := m.loadEntry(vaultID)
|
||||
if err != nil {
|
||||
m.logger.Printf("[ERR] expire: failed to read entry '%s': %v", vaultID, err)
|
||||
}
|
||||
|
||||
// Revoke the entry
|
||||
if err := m.revokeEntry(le); err != nil {
|
||||
m.logger.Printf("[ERR] expire: failed to revoke entry '%s': %v", vaultID, err)
|
||||
}
|
||||
|
||||
// Delete the entry
|
||||
if err := m.deleteEntry(vaultID); err != nil {
|
||||
m.logger.Printf("[ERR] expire: failed to delete entry '%s': %v", vaultID, err)
|
||||
}
|
||||
m.logger.Printf("[INFO] expire: revoked '%s'", vaultID)
|
||||
}
|
||||
|
||||
// revokeEntry is used to attempt revocation of an internal entry
|
||||
func (m *ExpirationManager) revokeEntry(le *leaseEntry) error {
|
||||
req := &logical.Request{
|
||||
Operation: logical.RevokeOperation,
|
||||
Path: le.Path,
|
||||
Data: le.Data,
|
||||
}
|
||||
_, err := m.router.Route(req)
|
||||
return err
|
||||
}
|
||||
|
||||
// loadEntry is used to read a lease entry
|
||||
func (m *ExpirationManager) loadEntry(vaultID string) (*leaseEntry, error) {
|
||||
out, err := m.view.Get(vaultID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read lease entry: %v", err)
|
||||
}
|
||||
if out == nil {
|
||||
return nil, nil
|
||||
}
|
||||
le, err := decodeLeaseEntry(out.Value)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode lease entry: %v", err)
|
||||
}
|
||||
return le, nil
|
||||
}
|
||||
|
||||
// persistEntry is used to persist a lease entry
|
||||
func (m *ExpirationManager) persistEntry(le *leaseEntry) error {
|
||||
// Encode the entry
|
||||
buf, err := le.encode()
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to encode lease entry: %v", err)
|
||||
return fmt.Errorf("failed to encode lease entry: %v", err)
|
||||
}
|
||||
|
||||
// Write out to the view
|
||||
|
|
@ -180,24 +224,29 @@ func (m *ExpirationManager) Register(req *logical.Request, resp *logical.Respons
|
|||
Value: buf,
|
||||
}
|
||||
if err := m.view.Put(&ent); err != nil {
|
||||
return "", fmt.Errorf("failed to persist lease entry: %v", err)
|
||||
return fmt.Errorf("failed to persist lease entry: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// TODO: Automatic revoke timer...
|
||||
|
||||
// Done
|
||||
return le.VaultID, nil
|
||||
// deleteEntry is used to delete a lease entry
|
||||
func (m *ExpirationManager) deleteEntry(vaultID string) error {
|
||||
if err := m.view.Delete(vaultID); err != nil {
|
||||
return fmt.Errorf("failed to delete lease entry: %v", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// leaseEntry is used to structure the values the expiration
|
||||
// manager stores. This is used to handle renew and revocation.
|
||||
type leaseEntry struct {
|
||||
VaultID string
|
||||
Path string
|
||||
Data map[string]interface{}
|
||||
Lease *logical.Lease
|
||||
IssueTime time.Time
|
||||
RenewTime time.Time
|
||||
VaultID string `json:"vault_id"`
|
||||
Path string `json:"path"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
Lease *logical.Lease `json:"lease"`
|
||||
IssueTime time.Time `json:"issue_time"`
|
||||
RenewTime time.Time `json:"renew_time"`
|
||||
RevokeAttempts int `json:"renew_attempts"`
|
||||
}
|
||||
|
||||
// encode is used to JSON encode the lease entry
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
package vault
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
|
@ -27,17 +29,19 @@ func mockExpiration(t *testing.T) *ExpirationManager {
|
|||
view := NewBarrierView(b, "expire/")
|
||||
|
||||
router := NewRouter()
|
||||
return NewExpirationManager(router, view)
|
||||
logger := log.New(os.Stderr, "", log.LstdFlags)
|
||||
return NewExpirationManager(router, view, logger)
|
||||
}
|
||||
|
||||
/*
|
||||
func TestExpiration_StartStop(t *testing.T) {
|
||||
exp := mockExpiration(t)
|
||||
err := exp.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
err := exp.Start()
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
||||
err = exp.Restore()
|
||||
err := exp.Restore()
|
||||
if err.Error() != "cannot restore while running" {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
|
|
@ -47,6 +51,7 @@ func TestExpiration_StartStop(t *testing.T) {
|
|||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
func TestExpiration_Register(t *testing.T) {
|
||||
exp := mockExpiration(t)
|
||||
|
|
|
|||
|
|
@ -3,14 +3,39 @@ package vault
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
// logical.Factory
|
||||
func PassthroughBackendFactory(map[string]string) (logical.Backend, error) {
|
||||
return new(PassthroughBackend), nil
|
||||
var b PassthroughBackend
|
||||
return &framework.Backend{
|
||||
Paths: []*framework.Path{
|
||||
&framework.Path{
|
||||
Pattern: ".*",
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"lease": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: "Lease time for this key when read. Ex: 1h",
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ReadOperation: b.handleRead,
|
||||
logical.WriteOperation: b.handleWrite,
|
||||
logical.DeleteOperation: b.handleDelete,
|
||||
logical.ListOperation: b.handleList,
|
||||
},
|
||||
|
||||
HelpSynopsis: strings.TrimSpace(passthroughHelpSynopsis),
|
||||
HelpDescription: strings.TrimSpace(passthroughHelpDescription),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PassthroughBackend is used storing secrets directly into the physical
|
||||
|
|
@ -19,28 +44,8 @@ func PassthroughBackendFactory(map[string]string) (logical.Backend, error) {
|
|||
// fancy.
|
||||
type PassthroughBackend struct{}
|
||||
|
||||
func (b *PassthroughBackend) HandleRequest(req *logical.Request) (*logical.Response, error) {
|
||||
// TODO(mitchellh): help, let's just do it when we migrate to helper/backend
|
||||
|
||||
switch req.Operation {
|
||||
case logical.ReadOperation:
|
||||
return b.handleRead(req)
|
||||
case logical.WriteOperation:
|
||||
return b.handleWrite(req)
|
||||
case logical.DeleteOperation:
|
||||
return b.handleDelete(req)
|
||||
case logical.ListOperation:
|
||||
return b.handleList(req)
|
||||
default:
|
||||
return nil, logical.ErrUnsupportedOperation
|
||||
}
|
||||
}
|
||||
|
||||
func (b *PassthroughBackend) RootPaths() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (b *PassthroughBackend) handleRead(req *logical.Request) (*logical.Response, error) {
|
||||
func (b *PassthroughBackend) handleRead(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
// Read the path
|
||||
out, err := req.Storage.Get(req.Path)
|
||||
if err != nil {
|
||||
|
|
@ -83,7 +88,8 @@ func (b *PassthroughBackend) handleRead(req *logical.Request) (*logical.Response
|
|||
return resp, nil
|
||||
}
|
||||
|
||||
func (b *PassthroughBackend) handleWrite(req *logical.Request) (*logical.Response, error) {
|
||||
func (b *PassthroughBackend) handleWrite(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
// Check that some fields are given
|
||||
if len(req.Data) == 0 {
|
||||
return nil, fmt.Errorf("missing data fields")
|
||||
|
|
@ -107,7 +113,8 @@ func (b *PassthroughBackend) handleWrite(req *logical.Request) (*logical.Respons
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *PassthroughBackend) handleDelete(req *logical.Request) (*logical.Response, error) {
|
||||
func (b *PassthroughBackend) handleDelete(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
// Delete the key at the request path
|
||||
if err := req.Storage.Delete(req.Path); err != nil {
|
||||
return nil, err
|
||||
|
|
@ -116,7 +123,8 @@ func (b *PassthroughBackend) handleDelete(req *logical.Request) (*logical.Respon
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (b *PassthroughBackend) handleList(req *logical.Request) (*logical.Response, error) {
|
||||
func (b *PassthroughBackend) handleList(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
// List the keys at the prefix given by the request
|
||||
keys, err := req.Storage.List(req.Path)
|
||||
if err != nil {
|
||||
|
|
@ -126,3 +134,19 @@ func (b *PassthroughBackend) handleList(req *logical.Request) (*logical.Response
|
|||
// Generate the response
|
||||
return logical.ListResponse(keys), nil
|
||||
}
|
||||
|
||||
const passthroughHelpSynopsis = `
|
||||
Pass-through secret storage to the physical backend, allowing you to
|
||||
read/write arbitrary data into secret storage.
|
||||
`
|
||||
|
||||
const passthroughHelpDescription = `
|
||||
The pass-through backend reads and writes arbitrary data into secret storage,
|
||||
encrypting it along the way.
|
||||
|
||||
A lease can be specified when writing with the "lease" field. If given, then
|
||||
when the secret is read, Vault will report a lease with that duration. It
|
||||
is expected that the consumer of this backend properly writes renewed keys
|
||||
before the lease is up. In addition, revocation must be handled by the
|
||||
user of this backend.
|
||||
`
|
||||
|
|
|
|||
|
|
@ -8,12 +8,8 @@ import (
|
|||
"github.com/hashicorp/vault/logical"
|
||||
)
|
||||
|
||||
func TestPassthroughBackend_impl(t *testing.T) {
|
||||
var _ logical.Backend = new(PassthroughBackend)
|
||||
}
|
||||
|
||||
func TestPassthroughBackend_RootPaths(t *testing.T) {
|
||||
var b PassthroughBackend
|
||||
b := testPassthroughBackend()
|
||||
root := b.RootPaths()
|
||||
if len(root) != 0 {
|
||||
t.Fatalf("unexpected: %v", root)
|
||||
|
|
@ -21,7 +17,7 @@ func TestPassthroughBackend_RootPaths(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPassthroughBackend_Write(t *testing.T) {
|
||||
var b PassthroughBackend
|
||||
b := testPassthroughBackend()
|
||||
req := logical.TestRequest(t, logical.WriteOperation, "foo")
|
||||
req.Data["raw"] = "test"
|
||||
|
||||
|
|
@ -43,7 +39,7 @@ func TestPassthroughBackend_Write(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPassthroughBackend_Read(t *testing.T) {
|
||||
var b PassthroughBackend
|
||||
b := testPassthroughBackend()
|
||||
req := logical.TestRequest(t, logical.WriteOperation, "foo")
|
||||
req.Data["raw"] = "test"
|
||||
req.Data["lease"] = "1h"
|
||||
|
|
@ -82,7 +78,7 @@ func TestPassthroughBackend_Read(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPassthroughBackend_Delete(t *testing.T) {
|
||||
var b PassthroughBackend
|
||||
b := testPassthroughBackend()
|
||||
req := logical.TestRequest(t, logical.WriteOperation, "foo")
|
||||
req.Data["raw"] = "test"
|
||||
storage := req.Storage
|
||||
|
|
@ -113,7 +109,7 @@ func TestPassthroughBackend_Delete(t *testing.T) {
|
|||
}
|
||||
|
||||
func TestPassthroughBackend_List(t *testing.T) {
|
||||
var b PassthroughBackend
|
||||
b := testPassthroughBackend()
|
||||
req := logical.TestRequest(t, logical.WriteOperation, "foo")
|
||||
req.Data["raw"] = "test"
|
||||
storage := req.Storage
|
||||
|
|
@ -141,3 +137,8 @@ func TestPassthroughBackend_List(t *testing.T) {
|
|||
t.Fatalf("bad response.\n\nexpected: %#v\n\nGot: %#v", expected, resp)
|
||||
}
|
||||
}
|
||||
|
||||
func testPassthroughBackend() logical.Backend {
|
||||
b, _ := PassthroughBackendFactory(nil)
|
||||
return b
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,8 +4,72 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/hashicorp/vault/logical"
|
||||
"github.com/hashicorp/vault/logical/framework"
|
||||
)
|
||||
|
||||
func NewSystemBackend(core *Core) logical.Backend {
|
||||
b := &SystemBackend{Core: core}
|
||||
|
||||
return &framework.Backend{
|
||||
PathsRoot: []string{
|
||||
"mounts/*",
|
||||
"remount",
|
||||
},
|
||||
|
||||
Paths: []*framework.Path{
|
||||
&framework.Path{
|
||||
Pattern: "mounts$",
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.ReadOperation: b.handleMountTable,
|
||||
},
|
||||
|
||||
HelpSynopsis: strings.TrimSpace(sysHelp["mounts"][0]),
|
||||
HelpDescription: strings.TrimSpace(sysHelp["mounts"][1]),
|
||||
},
|
||||
|
||||
&framework.Path{
|
||||
Pattern: "mounts/(?P<path>.+)",
|
||||
|
||||
Fields: map[string]*framework.FieldSchema{
|
||||
"path": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: strings.TrimSpace(sysHelp["mount_path"][0]),
|
||||
},
|
||||
|
||||
"type": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: strings.TrimSpace(sysHelp["mount_type"][0]),
|
||||
},
|
||||
"description": &framework.FieldSchema{
|
||||
Type: framework.TypeString,
|
||||
Description: strings.TrimSpace(sysHelp["mount_desc"][0]),
|
||||
},
|
||||
},
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.WriteOperation: b.handleMount,
|
||||
logical.DeleteOperation: b.handleUnmount,
|
||||
},
|
||||
|
||||
HelpSynopsis: strings.TrimSpace(sysHelp["mount"][0]),
|
||||
HelpDescription: strings.TrimSpace(sysHelp["mount"][1]),
|
||||
},
|
||||
|
||||
&framework.Path{
|
||||
Pattern: "remount",
|
||||
|
||||
Callbacks: map[logical.Operation]framework.OperationFunc{
|
||||
logical.WriteOperation: b.handleRemount,
|
||||
},
|
||||
|
||||
HelpSynopsis: strings.TrimSpace(sysHelp["remount"][0]),
|
||||
HelpDescription: strings.TrimSpace(sysHelp["remount"][1]),
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// SystemBackend implements logical.Backend and is used to interact with
|
||||
// the core of the system. This backend is hardcoded to exist at the "sys"
|
||||
// prefix. Conceptually it is similar to procfs on Linux.
|
||||
|
|
@ -13,35 +77,9 @@ type SystemBackend struct {
|
|||
Core *Core
|
||||
}
|
||||
|
||||
func (b *SystemBackend) HandleRequest(req *logical.Request) (*logical.Response, error) {
|
||||
// Switch on the path to route to the appropriate handler
|
||||
switch {
|
||||
case req.Path == "mounts":
|
||||
return b.handleMountTable(req)
|
||||
case strings.HasPrefix(req.Path, "mount/"):
|
||||
return b.handleMountOperation(req)
|
||||
case req.Path == "remount":
|
||||
return b.handleRemount(req)
|
||||
default:
|
||||
return nil, logical.ErrUnsupportedPath
|
||||
}
|
||||
}
|
||||
|
||||
func (b *SystemBackend) RootPaths() []string {
|
||||
return []string{
|
||||
"mount/*",
|
||||
"remount",
|
||||
}
|
||||
}
|
||||
|
||||
// handleMountTable handles the "mounts" endpoint to provide the mount table
|
||||
func (b *SystemBackend) handleMountTable(req *logical.Request) (*logical.Response, error) {
|
||||
switch req.Operation {
|
||||
case logical.ReadOperation:
|
||||
default:
|
||||
return nil, logical.ErrUnsupportedOperation
|
||||
}
|
||||
|
||||
func (b *SystemBackend) handleMountTable(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
b.Core.mountsLock.RLock()
|
||||
defer b.Core.mountsLock.RUnlock()
|
||||
|
||||
|
|
@ -60,35 +98,23 @@ func (b *SystemBackend) handleMountTable(req *logical.Request) (*logical.Respons
|
|||
return resp, nil
|
||||
}
|
||||
|
||||
// handleMountOperation is used to mount or unmount a path
|
||||
func (b *SystemBackend) handleMountOperation(req *logical.Request) (*logical.Response, error) {
|
||||
switch req.Operation {
|
||||
case logical.WriteOperation:
|
||||
return b.handleMount(req)
|
||||
case logical.DeleteOperation:
|
||||
return b.handleUnmount(req)
|
||||
default:
|
||||
return nil, logical.ErrUnsupportedOperation
|
||||
}
|
||||
}
|
||||
|
||||
// handleMount is used to mount a new path
|
||||
func (b *SystemBackend) handleMount(req *logical.Request) (*logical.Response, error) {
|
||||
suffix := strings.TrimPrefix(req.Path, "mount/")
|
||||
if len(suffix) == 0 {
|
||||
return logical.ErrorResponse("path cannot be blank"), logical.ErrInvalidRequest
|
||||
}
|
||||
func (b *SystemBackend) handleMount(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
// Get all the options
|
||||
path := data.Get("path").(string)
|
||||
logicalType := data.Get("type").(string)
|
||||
description := data.Get("description").(string)
|
||||
|
||||
// Get the type and description (optionally)
|
||||
logicalType := req.GetString("type")
|
||||
if logicalType == "" {
|
||||
return logical.ErrorResponse("backend type must be specified as a string"), logical.ErrInvalidRequest
|
||||
return logical.ErrorResponse(
|
||||
"backend type must be specified as a string"),
|
||||
logical.ErrInvalidRequest
|
||||
}
|
||||
description := req.GetString("description")
|
||||
|
||||
// Create the mount entry
|
||||
me := &MountEntry{
|
||||
Path: suffix,
|
||||
Path: path,
|
||||
Type: logicalType,
|
||||
Description: description,
|
||||
}
|
||||
|
|
@ -101,8 +127,9 @@ func (b *SystemBackend) handleMount(req *logical.Request) (*logical.Response, er
|
|||
}
|
||||
|
||||
// handleUnmount is used to unmount a path
|
||||
func (b *SystemBackend) handleUnmount(req *logical.Request) (*logical.Response, error) {
|
||||
suffix := strings.TrimPrefix(req.Path, "mount/")
|
||||
func (b *SystemBackend) handleUnmount(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
suffix := strings.TrimPrefix(req.Path, "mounts/")
|
||||
if len(suffix) == 0 {
|
||||
return logical.ErrorResponse("path cannot be blank"), logical.ErrInvalidRequest
|
||||
}
|
||||
|
|
@ -116,7 +143,8 @@ func (b *SystemBackend) handleUnmount(req *logical.Request) (*logical.Response,
|
|||
}
|
||||
|
||||
// handleRemount is used to remount a path
|
||||
func (b *SystemBackend) handleRemount(req *logical.Request) (*logical.Response, error) {
|
||||
func (b *SystemBackend) handleRemount(
|
||||
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||
// Only accept write operations
|
||||
switch req.Operation {
|
||||
case logical.WriteOperation:
|
||||
|
|
@ -140,3 +168,46 @@ func (b *SystemBackend) handleRemount(req *logical.Request) (*logical.Response,
|
|||
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// sysHelp is all the help text for the sys backend.
|
||||
var sysHelp = map[string][2]string{
|
||||
"mounts": {
|
||||
"List the currently mounted backends.",
|
||||
`
|
||||
List the currently mounted backends: the mount path, the type of the backend,
|
||||
and a user friendly description of the purpose for the mount.
|
||||
`,
|
||||
},
|
||||
|
||||
"mount": {
|
||||
`Mount a new backend at a new path.`,
|
||||
`
|
||||
Mount a backend at a new path. A backend can be mounted multiple times at
|
||||
multiple paths in order to configure multiple separately configured backends.
|
||||
Example: you might have an AWS backend for the east coast, and one for the
|
||||
west coast.
|
||||
`,
|
||||
},
|
||||
|
||||
"mount_path": {
|
||||
`The path to mount to. Example: "aws/east"`,
|
||||
"",
|
||||
},
|
||||
|
||||
"mount_type": {
|
||||
`The type of the backend. Example: "passthrough"`,
|
||||
"",
|
||||
},
|
||||
|
||||
"mount_desc": {
|
||||
`User-friendly description for this mount.`,
|
||||
"",
|
||||
},
|
||||
|
||||
"remount": {
|
||||
"Move the mount point of an already-mounted backend.",
|
||||
`
|
||||
Change the mount point of an already-mounted backend.
|
||||
`,
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,13 +7,9 @@ import (
|
|||
"github.com/hashicorp/vault/logical"
|
||||
)
|
||||
|
||||
func TestSystemBackend_impl(t *testing.T) {
|
||||
var _ logical.Backend = new(SystemBackend)
|
||||
}
|
||||
|
||||
func TestSystemBackend_RootPaths(t *testing.T) {
|
||||
expected := []string{
|
||||
"mount/*",
|
||||
"mounts/*",
|
||||
"remount",
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +46,7 @@ func TestSystemBackend_mounts(t *testing.T) {
|
|||
func TestSystemBackend_mount(t *testing.T) {
|
||||
b := testSystemBackend(t)
|
||||
|
||||
req := logical.TestRequest(t, logical.WriteOperation, "mount/prod/secret/")
|
||||
req := logical.TestRequest(t, logical.WriteOperation, "mounts/prod/secret/")
|
||||
req.Data["type"] = "generic"
|
||||
|
||||
resp, err := b.HandleRequest(req)
|
||||
|
|
@ -65,13 +61,13 @@ func TestSystemBackend_mount(t *testing.T) {
|
|||
func TestSystemBackend_mount_invalid(t *testing.T) {
|
||||
b := testSystemBackend(t)
|
||||
|
||||
req := logical.TestRequest(t, logical.WriteOperation, "mount/prod/secret/")
|
||||
req := logical.TestRequest(t, logical.WriteOperation, "mounts/prod/secret/")
|
||||
req.Data["type"] = "nope"
|
||||
resp, err := b.HandleRequest(req)
|
||||
if err != logical.ErrInvalidRequest {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
if resp.Data["error"] != "unknown logical backend type: nope" {
|
||||
if resp.Data["error"] != "unknown backend type: nope" {
|
||||
t.Fatalf("bad: %v", resp)
|
||||
}
|
||||
}
|
||||
|
|
@ -79,7 +75,7 @@ func TestSystemBackend_mount_invalid(t *testing.T) {
|
|||
func TestSystemBackend_unmount(t *testing.T) {
|
||||
b := testSystemBackend(t)
|
||||
|
||||
req := logical.TestRequest(t, logical.DeleteOperation, "mount/secret/")
|
||||
req := logical.TestRequest(t, logical.DeleteOperation, "mounts/secret/")
|
||||
resp, err := b.HandleRequest(req)
|
||||
if err != nil {
|
||||
t.Fatalf("err: %v", err)
|
||||
|
|
@ -92,7 +88,7 @@ func TestSystemBackend_unmount(t *testing.T) {
|
|||
func TestSystemBackend_unmount_invalid(t *testing.T) {
|
||||
b := testSystemBackend(t)
|
||||
|
||||
req := logical.TestRequest(t, logical.DeleteOperation, "mount/foo/")
|
||||
req := logical.TestRequest(t, logical.DeleteOperation, "mounts/foo/")
|
||||
resp, err := b.HandleRequest(req)
|
||||
if err != logical.ErrInvalidRequest {
|
||||
t.Fatalf("err: %v", err)
|
||||
|
|
@ -147,7 +143,7 @@ func TestSystemBackend_remount_system(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func testSystemBackend(t *testing.T) *SystemBackend {
|
||||
func testSystemBackend(t *testing.T) logical.Backend {
|
||||
c, _ := TestCoreUnsealed(t)
|
||||
return &SystemBackend{Core: c}
|
||||
return NewSystemBackend(c)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,25 +9,6 @@ import (
|
|||
"github.com/hashicorp/vault/logical"
|
||||
)
|
||||
|
||||
// TEMPORARY!
|
||||
|
||||
// BuiltinBackends contains all of the available backends
|
||||
var BuiltinBackends = map[string]logical.Factory{
|
||||
"generic": PassthroughBackendFactory,
|
||||
}
|
||||
|
||||
// NewBackend returns a new logical Backend with the given type and configuration.
|
||||
// The backend is looked up in the BuiltinBackends variable.
|
||||
func NewBackend(t string, conf map[string]string) (logical.Backend, error) {
|
||||
f, ok := BuiltinBackends[t]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown logical backend type: %s", t)
|
||||
}
|
||||
return f(conf)
|
||||
}
|
||||
|
||||
// TEMPORARY!
|
||||
|
||||
const (
|
||||
// coreMountConfigPath is used to store the mount configuration.
|
||||
// Mounts are protected within the Vault itself, which means they
|
||||
|
|
@ -103,7 +84,7 @@ func (c *Core) mount(me *MountEntry) error {
|
|||
}
|
||||
|
||||
// Lookup the new backend
|
||||
backend, err := NewBackend(me.Type, nil)
|
||||
backend, err := c.newBackend(me.Type, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
@ -288,24 +269,29 @@ func (c *Core) setupMounts() error {
|
|||
var err error
|
||||
for _, entry := range c.mounts.Entries {
|
||||
// Initialize the backend, special casing for system
|
||||
barrierPrefix := backendBarrierPrefix
|
||||
if entry.Type == "system" {
|
||||
barrierPrefix = systemBarrierPrefix
|
||||
}
|
||||
|
||||
backend, err = c.newBackend(entry.Type, nil)
|
||||
if err != nil {
|
||||
c.logger.Printf(
|
||||
"[ERR] core: failed to create mount entry %#v: %v",
|
||||
entry, err)
|
||||
return loadMountsFailed
|
||||
}
|
||||
|
||||
// Create a barrier view using the UUID
|
||||
view = NewBarrierView(c.barrier, barrierPrefix+entry.UUID+"/")
|
||||
|
||||
if entry.Type == "system" {
|
||||
backend = &SystemBackend{Core: c}
|
||||
view = NewBarrierView(c.barrier, systemBarrierPrefix+entry.UUID+"/")
|
||||
c.systemView = view
|
||||
|
||||
} else {
|
||||
backend, err = NewBackend(entry.Type, nil)
|
||||
if err != nil {
|
||||
c.logger.Printf("[ERR] core: failed to create mount entry %#v: %v", entry, err)
|
||||
return loadMountsFailed
|
||||
}
|
||||
|
||||
// Create a barrier view using the UUID
|
||||
view = NewBarrierView(c.barrier, backendBarrierPrefix+entry.UUID+"/")
|
||||
}
|
||||
|
||||
// Mount the backend
|
||||
if err := c.router.Mount(backend, entry.Type, entry.Path, view); err != nil {
|
||||
err = c.router.Mount(backend, entry.Type, entry.Path, view)
|
||||
if err != nil {
|
||||
c.logger.Printf("[ERR] core: failed to mount entry %#v: %v", entry, err)
|
||||
return loadMountsFailed
|
||||
}
|
||||
|
|
@ -322,6 +308,15 @@ func (c *Core) unloadMounts() error {
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *Core) newBackend(t string, conf map[string]string) (logical.Backend, error) {
|
||||
f, ok := c.backends[t]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown backend type: %s", t)
|
||||
}
|
||||
|
||||
return f(conf)
|
||||
}
|
||||
|
||||
// defaultMountTable creates a default mount table
|
||||
func defaultMountTable() *MountTable {
|
||||
table := &MountTable{}
|
||||
|
|
|
|||
31
website/source/_ember_templates.html.erb
Normal file
31
website/source/_ember_templates.html.erb
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
<!-- TODO Precompile ember templates -->
|
||||
|
||||
<script type="text/x-handlebars" data-template-name="application">
|
||||
{{outlet}}
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" data-template-name="demo">
|
||||
<div class="terminal">
|
||||
{{outlet}}
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="text/x-handlebars" data-template-name="demo/crud">
|
||||
{{#if notCleared}}
|
||||
<div class="welcome">
|
||||
Any Vault command you run passes through remotely to
|
||||
the real Vault interface, so feel free to explore, but
|
||||
be careful of the values you set.
|
||||
</div>
|
||||
{{/if}}
|
||||
|
||||
<div class="log">
|
||||
{{#each line in currentLog}}
|
||||
{{logPrefix}}{{line}}
|
||||
{{/each}}
|
||||
</div>
|
||||
|
||||
<form {{action "submitText" on="submit"}}>
|
||||
{{logPrefix}} {{input value=currentText class="shell" spellcheck="false"}}
|
||||
</form>
|
||||
</script>
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 3.2 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 22 KiB After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 34 KiB |
130
website/source/assets/javascripts/app/DotLockup.js
Normal file
130
website/source/assets/javascripts/app/DotLockup.js
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
(function(){
|
||||
|
||||
DotLockup = Base.extend({
|
||||
|
||||
$keyWrap: null,
|
||||
$keys: null,
|
||||
|
||||
constructor: function(){
|
||||
var _this = this;
|
||||
|
||||
_this.$keyWrap = $('.keys');
|
||||
_this.$keys = $('.keys span');
|
||||
|
||||
//(3000)
|
||||
|
||||
_this.addEventListeners();
|
||||
_this.animateFull()
|
||||
.then(_this.animateOff.bind(this))
|
||||
.then(_this.animateFull.bind(this))
|
||||
.then(_this.animatePress.bind(this))
|
||||
.then(_this.resetKeys.bind(this));
|
||||
},
|
||||
|
||||
addEventListeners: function(){
|
||||
var _this = this;
|
||||
},
|
||||
|
||||
animateFull: function(uberDelay){
|
||||
var _this = this,
|
||||
uberDelay = uberDelay || 0,
|
||||
deferred = $.Deferred();
|
||||
|
||||
setTimeout( function(){
|
||||
_this.updateEachKeyClass('full', 'off', 1000, 150, deferred.resolve);
|
||||
}, uberDelay)
|
||||
|
||||
return deferred;
|
||||
},
|
||||
|
||||
animateOff: function(){
|
||||
var deferred = $.Deferred();
|
||||
|
||||
this.updateEachKeyClass('full off', '', 1250, 150, deferred.resolve, true);
|
||||
|
||||
return deferred;
|
||||
},
|
||||
|
||||
animatePress: function(){
|
||||
var _this = this,
|
||||
deferred = $.Deferred(),
|
||||
len = _this.$keys.length,
|
||||
presses = _this.randomNumbersIn(len),
|
||||
delay = 250,
|
||||
interval = 1000;
|
||||
|
||||
for(var i=0; i < len; i++){
|
||||
(function(index){
|
||||
setTimeout(function(){
|
||||
_this.$keys.eq(presses[index]).addClass('press');
|
||||
if(index == len -1 ){
|
||||
deferred.resolve();
|
||||
}
|
||||
}, delay)
|
||||
|
||||
delay += interval;
|
||||
}(i))
|
||||
}
|
||||
|
||||
return deferred;
|
||||
},
|
||||
|
||||
resetKeys: function(){
|
||||
var _this = this,
|
||||
len = _this.$keys.length,
|
||||
delay = 2500,
|
||||
interval = 250;
|
||||
|
||||
setTimeout(function(){
|
||||
_this.$keys.removeClass('full press');
|
||||
}, delay)
|
||||
/*for(var i=0; i < len; i++){
|
||||
(function(index){
|
||||
setTimeout(function(){
|
||||
_this.$keys.eq(index).removeClass('full press');
|
||||
}, delay)
|
||||
|
||||
delay += interval;
|
||||
}(i))
|
||||
}*/
|
||||
},
|
||||
|
||||
updateEachKeyClass: function(clsAdd, clsRemove, delay, interval, resolve, reverse){
|
||||
var delay = delay;
|
||||
this.$keys.each(function(index){
|
||||
var span = this;
|
||||
var finishIndex = (reverse) ? 0 : 9; // final timeout at 0 or 9 depending on if class removal is reversed on the span list
|
||||
setTimeout( function(){
|
||||
$(span).removeClass(clsRemove).addClass(clsAdd);
|
||||
if(index == finishIndex ){
|
||||
resolve();
|
||||
}
|
||||
}, delay);
|
||||
|
||||
if(reverse){
|
||||
delay -= interval;
|
||||
}else{
|
||||
delay += interval;
|
||||
}
|
||||
})
|
||||
|
||||
},
|
||||
|
||||
randomNumbersIn: function(len){
|
||||
var arr = [];
|
||||
while(arr.length < len){
|
||||
var randomnumber=Math.floor(Math.random()*len)
|
||||
var found=false;
|
||||
for(var i=0;i<arr.length;i++){
|
||||
if(arr[i]==randomnumber){found=true;break}
|
||||
}
|
||||
if(!found)arr[arr.length]=randomnumber;
|
||||
}
|
||||
return arr;
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
window.DotLockup = DotLockup;
|
||||
|
||||
})();
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
(function(Sidebar){
|
||||
(function(Sidebar, DotLockup){
|
||||
|
||||
// Quick and dirty IE detection
|
||||
var isIE = (function(){
|
||||
|
|
@ -25,6 +25,10 @@ var Init = {
|
|||
new Sidebar();
|
||||
},
|
||||
|
||||
initializeDotLockup: function(){
|
||||
new DotLockup();
|
||||
},
|
||||
|
||||
initializeWaypoints: function(){
|
||||
$('#header').waypoint(function(event, direction) {
|
||||
$(this.element).addClass('showit');
|
||||
|
|
@ -46,6 +50,7 @@ var Init = {
|
|||
Pages: {
|
||||
'page-home': function(){
|
||||
Init.initializeSidebar();
|
||||
Init.initializeDotLockup();
|
||||
Init.initializeWaypoints();
|
||||
}
|
||||
}
|
||||
|
|
@ -54,4 +59,4 @@ var Init = {
|
|||
|
||||
Init.start();
|
||||
|
||||
})(window.Sidebar);
|
||||
})(window.Sidebar, window.DotLockup);
|
||||
|
|
@ -1,6 +1,8 @@
|
|||
//= require jquery
|
||||
//= require bootstrap
|
||||
//= require jquery.waypoints.min
|
||||
//= require lib/ember-template-compiler
|
||||
//= require lib/ember-1-10.min
|
||||
|
||||
//= require lib/String.substitute
|
||||
//= require lib/Function.prototype.bind
|
||||
|
|
@ -10,4 +12,8 @@
|
|||
|
||||
//= require docs
|
||||
//= require app/Sidebar
|
||||
//= require app/DotLockup
|
||||
//= require app/Init
|
||||
|
||||
//= require demo
|
||||
//= require_tree ./demo
|
||||
|
|
|
|||
9
website/source/assets/javascripts/demo.js
Normal file
9
website/source/assets/javascripts/demo.js
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
window.Demo = Ember.Application.create({
|
||||
rootElement: '#demo-app',
|
||||
});
|
||||
|
||||
Demo.deferReadiness();
|
||||
|
||||
if (document.getElementById('demo-app')) {
|
||||
Demo.advanceReadiness();
|
||||
}
|
||||
33
website/source/assets/javascripts/demo/controllers/crud.js
Normal file
33
website/source/assets/javascripts/demo/controllers/crud.js
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
Demo.DemoCrudController = Ember.ObjectController.extend({
|
||||
needs: ['demo'],
|
||||
currentText: Ember.computed.alias('controllers.demo.currentText'),
|
||||
currentLog: Ember.computed.alias('controllers.demo.currentLog'),
|
||||
logPrefix: Ember.computed.alias('controllers.demo.logPrefix'),
|
||||
currentMarker: Ember.computed.alias('controllers.demo.currentMarker'),
|
||||
notCleared: Ember.computed.alias('controllers.demo.notCleared'),
|
||||
|
||||
actions: {
|
||||
submitText: function() {
|
||||
var command = this.getWithDefault('currentText', '');
|
||||
var currentLogs = this.get('currentLog').toArray();
|
||||
|
||||
// Add the last log item
|
||||
currentLogs.push(command);
|
||||
|
||||
// Clean the state
|
||||
this.set('currentText', '');
|
||||
|
||||
// Push the new logs
|
||||
this.set('currentLog', currentLogs);
|
||||
|
||||
switch(command) {
|
||||
case "clear":
|
||||
this.set('currentLog', []);
|
||||
this.set('notCleared', false);
|
||||
break;
|
||||
default:
|
||||
console.log("Submitting: ", command);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
13
website/source/assets/javascripts/demo/controllers/demo.js
Normal file
13
website/source/assets/javascripts/demo/controllers/demo.js
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
Demo.DemoController = Ember.ObjectController.extend({
|
||||
currentText: "vault help",
|
||||
currentLog: [],
|
||||
logPrefix: "$ ",
|
||||
cursor: 0,
|
||||
notCleared: true,
|
||||
|
||||
setFromHistory: function() {
|
||||
var index = this.get('currentLog.length') + this.get('cursor');
|
||||
|
||||
this.set('currentText', this.get('currentLog')[index]);
|
||||
}.observes('cursor')
|
||||
});
|
||||
5
website/source/assets/javascripts/demo/router.js
Normal file
5
website/source/assets/javascripts/demo/router.js
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
Demo.Router.map(function() {
|
||||
this.route('demo', { path: '/demo' }, function() {
|
||||
this.route('crud', { path: '/crud' });
|
||||
});
|
||||
});
|
||||
2
website/source/assets/javascripts/demo/routes/crud.js
Normal file
2
website/source/assets/javascripts/demo/routes/crud.js
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
Demo.DemoCrudRoute = Ember.Route.extend({
|
||||
});
|
||||
111
website/source/assets/javascripts/demo/views/demo.js
Normal file
111
website/source/assets/javascripts/demo/views/demo.js
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
Demo.DemoView = Ember.View.extend({
|
||||
classNames: ['demo-overlay'],
|
||||
|
||||
didInsertElement: function() {
|
||||
var element = this.$();
|
||||
|
||||
element.hide().fadeIn(300);
|
||||
|
||||
// Scroll to the bottom of the element
|
||||
element.scrollTop(element[0].scrollHeight);
|
||||
|
||||
// Focus
|
||||
element.find('input.shell')[0].focus();
|
||||
|
||||
// Hijack scrolling to only work within terminal
|
||||
//
|
||||
$(element).on('DOMMouseScroll mousewheel', function(ev) {
|
||||
var scrolledEl = $(this),
|
||||
scrollTop = this.scrollTop,
|
||||
scrollHeight = this.scrollHeight,
|
||||
height = scrolledEl.height(),
|
||||
delta = (ev.type == 'DOMMouseScroll' ?
|
||||
ev.originalEvent.detail * -40 :
|
||||
ev.originalEvent.wheelDelta),
|
||||
up = delta > 0;
|
||||
|
||||
var prevent = function() {
|
||||
ev.stopPropagation();
|
||||
ev.preventDefault();
|
||||
ev.returnValue = false;
|
||||
return false;
|
||||
};
|
||||
|
||||
if (!up && -delta > scrollHeight - height - scrollTop) {
|
||||
// Scrolling down, but this will take us past the bottom.
|
||||
scrolledEl.scrollTop(scrollHeight);
|
||||
return prevent();
|
||||
} else if (up && delta > scrollTop) {
|
||||
// Scrolling up, but this will take us past the top.
|
||||
scrolledEl.scrollTop(0);
|
||||
return prevent();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
willDestroyElement: function() {
|
||||
var element = this.$();
|
||||
|
||||
element.fadeOut(400);
|
||||
|
||||
// Allow scrolling
|
||||
$('body').unbind('DOMMouseScroll mousewheel');
|
||||
},
|
||||
|
||||
click: function() {
|
||||
var element = this.$();
|
||||
// Focus
|
||||
element.find('input.shell')[0].focus();
|
||||
},
|
||||
|
||||
keyDown: function(ev) {
|
||||
var cursor = this.get('controller.cursor'),
|
||||
currentLength = this.get('controller.currentLog.length');
|
||||
|
||||
console.log(ev);
|
||||
|
||||
switch(ev.keyCode) {
|
||||
// Down arrow
|
||||
case 40:
|
||||
if (cursor === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.incrementProperty('controller.cursor');
|
||||
break;
|
||||
|
||||
// Up arrow
|
||||
case 38:
|
||||
if ((currentLength + cursor) === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.decrementProperty('controller.cursor');
|
||||
break;
|
||||
|
||||
// command + k
|
||||
case 75:
|
||||
if (ev.metaKey) {
|
||||
this.set('controller.currentLog', []);
|
||||
this.set('controller.notCleared', false);
|
||||
}
|
||||
break;
|
||||
|
||||
// escape
|
||||
case 27:
|
||||
this.get('controller').transitionTo('index');
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
submitted: function() {
|
||||
var element = this.$();
|
||||
|
||||
// Focus the input
|
||||
element.find('input.shell')[0].focus();
|
||||
|
||||
// Scroll to the bottom of the element
|
||||
element.scrollTop(element[0].scrollHeight);
|
||||
|
||||
}.observes('controller.currentLog')
|
||||
});
|
||||
12
website/source/assets/javascripts/lib/ember-1-10.min.js
vendored
Normal file
12
website/source/assets/javascripts/lib/ember-1-10.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
38
website/source/assets/stylesheets/_demo.scss
Normal file
38
website/source/assets/stylesheets/_demo.scss
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
.demo-overlay {
|
||||
z-index: 100;
|
||||
bottom: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 80%;
|
||||
max-height: 80%;
|
||||
position: fixed;
|
||||
background-color: black;
|
||||
color: #DDDDDD;
|
||||
overflow: scroll;
|
||||
font-size: 18px;
|
||||
font-family: 'Ubuntu Mono', 'Monaco', monospace;
|
||||
}
|
||||
|
||||
.terminal {
|
||||
padding: 0px 25px;
|
||||
padding-bottom: 50px;
|
||||
|
||||
.welcome {
|
||||
padding-top: 20px;
|
||||
}
|
||||
|
||||
.log {
|
||||
white-space: pre;
|
||||
}
|
||||
|
||||
input.shell {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
display: inline-block;
|
||||
bottom: 0;
|
||||
width: 90%;
|
||||
background-color: black;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
}
|
||||
}
|
||||
|
|
@ -27,6 +27,7 @@
|
|||
left: 15px;
|
||||
top: 8px;
|
||||
width: 12px;
|
||||
|
||||
span{
|
||||
width: 3px;
|
||||
height: 3px;
|
||||
|
|
@ -35,9 +36,10 @@
|
|||
margin-left: 1px;
|
||||
margin-bottom: 1px;
|
||||
background-color: $blue;
|
||||
transition: all 250ms ease-in;
|
||||
|
||||
&:nth-child(1){
|
||||
opacity: 1;
|
||||
opacity: .85;
|
||||
}
|
||||
|
||||
&:nth-child(2){
|
||||
|
|
@ -49,15 +51,15 @@
|
|||
}
|
||||
|
||||
&:nth-child(4){
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
&:nth-child(5){
|
||||
opacity: .9;
|
||||
}
|
||||
|
||||
&:nth-child(5){
|
||||
opacity: .8;
|
||||
}
|
||||
|
||||
&:nth-child(6){
|
||||
opacity: .4;
|
||||
opacity: .5;
|
||||
}
|
||||
|
||||
&:nth-child(7){
|
||||
|
|
@ -76,6 +78,28 @@
|
|||
opacity: 1;
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
&.full{
|
||||
opacity: 1;
|
||||
transition: all 250ms ease-in;
|
||||
}
|
||||
|
||||
&.full.off{
|
||||
opacity: .55;
|
||||
transition: all 250ms ease-in;
|
||||
}
|
||||
|
||||
&.full.press{
|
||||
|
||||
animation-name: press;
|
||||
-webkit-animation-name: press;
|
||||
|
||||
animation-duration: 1s;
|
||||
-webkit-animation-duration: 1s;
|
||||
|
||||
animation-timing-function: ease;
|
||||
-webkit-animation-timing-function: ease;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -69,4 +69,29 @@
|
|||
|
||||
@mixin bez-1-transition{
|
||||
@include transition( all 300ms ease-in-out );
|
||||
}
|
||||
|
||||
|
||||
@keyframes press {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: .55
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes press {
|
||||
0% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: .55
|
||||
}
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
|
@ -1,7 +1,7 @@
|
|||
@import 'bootstrap-sprockets';
|
||||
@import 'bootstrap';
|
||||
|
||||
@import url("//fonts.googleapis.com/css?family=Lato:300,400,700|Open+Sans:300,600,400");
|
||||
@import url("//fonts.googleapis.com/css?family=Lato:300,400,700|Open+Sans:300,600,400|Ubuntu+Mono");
|
||||
|
||||
// Core variables and mixins
|
||||
@import '_variables';
|
||||
|
|
@ -28,3 +28,7 @@
|
|||
@import '_community';
|
||||
@import '_docs';
|
||||
@import '_downloads';
|
||||
|
||||
// Demo
|
||||
@import '_demo';
|
||||
|
||||
|
|
|
|||
|
|
@ -29,6 +29,8 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<div id="demo-app"></div>
|
||||
|
||||
<div id="content">
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
|
|
@ -53,7 +55,7 @@
|
|||
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Maecenas et purus at orci cursus mattis. Maecenas ullamcorper dictum elit. Vivamus sit amet nisi eu lacus lacinia iaculis. Nulla non massa ultricies, placerat lectus vel, mattis mauris. Nullam urna risus, volutpat quis viverra in, convallis at magna.
|
||||
<div class="feature-footer">
|
||||
<a class="v-btn black sml" href="/intro">Learn more</a>
|
||||
<a class="v-btn black sml terminal" href="/intro">Launch Interactive Terminal</a>
|
||||
<a class="v-btn black sml terminal" href="/#/demo/crud">Launch Interactive Terminal</a>
|
||||
</div>
|
||||
</p>
|
||||
</div> <!-- .feature -->
|
||||
|
|
|
|||
|
|
@ -1,4 +1,8 @@
|
|||
<%= partial "layouts/meta" %>
|
||||
<%= partial "layouts/header" %>
|
||||
|
||||
<%= yield %>
|
||||
|
||||
<%= partial "ember_templates" %>
|
||||
<%= partial "layouts/footer" %>
|
||||
|
||||
|
|
|
|||
|
|
@ -1,93 +0,0 @@
|
|||
{
|
||||
"name": "bootstrap",
|
||||
"description": "The most popular front-end framework for developing responsive, mobile first projects on the web.",
|
||||
"version": "3.3.2",
|
||||
"keywords": [
|
||||
"css",
|
||||
"less",
|
||||
"mobile-first",
|
||||
"responsive",
|
||||
"front-end",
|
||||
"framework",
|
||||
"web"
|
||||
],
|
||||
"homepage": "http://getbootstrap.com",
|
||||
"author": "Twitter, Inc.",
|
||||
"scripts": {
|
||||
"test": "grunt test"
|
||||
},
|
||||
"style": "dist/css/bootstrap.css",
|
||||
"less": "less/bootstrap.less",
|
||||
"main": "./dist/js/npm",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/twbs/bootstrap.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/twbs/bootstrap/issues"
|
||||
},
|
||||
"license": {
|
||||
"type": "MIT",
|
||||
"url": "https://github.com/twbs/bootstrap/blob/master/LICENSE"
|
||||
},
|
||||
"devDependencies": {
|
||||
"btoa": "~1.1.2",
|
||||
"glob": "~4.4.0",
|
||||
"grunt": "~0.4.5",
|
||||
"grunt-autoprefixer": "~2.2.0",
|
||||
"grunt-banner": "~0.3.1",
|
||||
"grunt-contrib-clean": "~0.6.0",
|
||||
"grunt-contrib-compress": "~0.13.0",
|
||||
"grunt-contrib-concat": "~0.5.1",
|
||||
"grunt-contrib-connect": "~0.9.0",
|
||||
"grunt-contrib-copy": "~0.8.0",
|
||||
"grunt-contrib-csslint": "~0.4.0",
|
||||
"grunt-contrib-cssmin": "~0.12.2",
|
||||
"grunt-contrib-jade": "~0.14.1",
|
||||
"grunt-contrib-jshint": "~0.11.0",
|
||||
"grunt-contrib-less": "~1.0.0",
|
||||
"grunt-contrib-qunit": "~0.5.2",
|
||||
"grunt-contrib-uglify": "~0.8.0",
|
||||
"grunt-contrib-watch": "~0.6.1",
|
||||
"grunt-csscomb": "~3.0.0",
|
||||
"grunt-exec": "~0.4.6",
|
||||
"grunt-html": "~3.0.0",
|
||||
"grunt-jekyll": "~0.4.2",
|
||||
"grunt-jscs": "~1.5.0",
|
||||
"grunt-saucelabs": "~8.6.0",
|
||||
"grunt-sed": "~0.1.1",
|
||||
"load-grunt-tasks": "~3.1.0",
|
||||
"markdown-it": "^3.0.7",
|
||||
"npm-shrinkwrap": "^200.1.0",
|
||||
"time-grunt": "^1.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.1"
|
||||
},
|
||||
"files": [
|
||||
"dist",
|
||||
"fonts",
|
||||
"grunt/*.js",
|
||||
"grunt/*.json",
|
||||
"js/*.js",
|
||||
"less/**/*.less",
|
||||
"Gruntfile.js",
|
||||
"LICENSE"
|
||||
],
|
||||
"jspm": {
|
||||
"main": "js/bootstrap",
|
||||
"directories": {
|
||||
"example": "examples",
|
||||
"lib": "dist"
|
||||
},
|
||||
"shim": {
|
||||
"js/bootstrap": {
|
||||
"imports": "jquery",
|
||||
"exports": "$"
|
||||
}
|
||||
},
|
||||
"buildConfig": {
|
||||
"uglify": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue