prototpe key animation

This commit is contained in:
captainill 2015-03-16 14:21:29 -07:00
commit 005058374e
63 changed files with 1950 additions and 441 deletions

1
.gitignore vendored
View file

@ -29,3 +29,4 @@ pkg/
# Vault-specific
example.hcl
example.vault.d

37
api/logical.go Normal file
View 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
}

View file

@ -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))
}

View file

@ -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
}

View file

@ -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
View 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
View 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())
}
}

View file

@ -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)
}

View file

@ -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
View 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())
}
}

View file

@ -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 {

View file

@ -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
View 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
View 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)
}
}

View file

@ -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

View file

@ -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
}

View file

@ -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
View 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
View 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
View 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
View 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)
}
}

View file

@ -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{}

View file

@ -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()

View file

@ -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.

View file

@ -1,4 +1,4 @@
package backend
package framework
import (
"reflect"

View file

@ -1,4 +1,4 @@
package backend
package framework
import (
"fmt"

View file

@ -1,4 +1,4 @@
package backend
package framework
import (
"reflect"

View file

@ -1,4 +1,4 @@
package backend
package framework
//go:generate stringer -type=FieldType field_type.go

View file

@ -1,6 +1,6 @@
// generated by stringer -type=FieldType field_type.go; DO NOT EDIT
package backend
package framework
import "fmt"

View file

@ -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.

View file

@ -1,4 +1,4 @@
package backend
package testing
import (
"os"

View file

@ -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
View 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)
}

View file

@ -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
}

View file

@ -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

View file

@ -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)

View file

@ -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.
`

View file

@ -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
}

View file

@ -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.
`,
},
}

View file

@ -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)
}

View file

@ -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{}

View 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

View 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;
})();

View file

@ -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);

View file

@ -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

View file

@ -0,0 +1,9 @@
window.Demo = Ember.Application.create({
rootElement: '#demo-app',
});
Demo.deferReadiness();
if (document.getElementById('demo-app')) {
Demo.advanceReadiness();
}

View 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);
}
}
}
});

View 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')
});

View file

@ -0,0 +1,5 @@
Demo.Router.map(function() {
this.route('demo', { path: '/demo' }, function() {
this.route('crud', { path: '/crud' });
});
});

View file

@ -0,0 +1,2 @@
Demo.DemoCrudRoute = Ember.Route.extend({
});

View 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')
});

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View 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;
}
}

View file

@ -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;
}
}
}
}

View file

@ -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;
}
}

View file

@ -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';

View file

@ -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 -->

View file

@ -1,4 +1,8 @@
<%= partial "layouts/meta" %>
<%= partial "layouts/header" %>
<%= yield %>
<%= partial "ember_templates" %>
<%= partial "layouts/footer" %>

View file

@ -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
}
}
}