logical: Adding generic backend

This commit is contained in:
Armon Dadgar 2015-03-05 16:35:09 -08:00
parent e997fd31ea
commit 51cc19e92f
3 changed files with 392 additions and 0 deletions

157
logical/generic.go Normal file
View File

@ -0,0 +1,157 @@
package logical
import (
"encoding/json"
"fmt"
"time"
"github.com/hashicorp/vault/vault"
)
func init() {
// Register the generic backend
vault.BuiltinBackends["generic"] = newGenericBackend
}
// GenericBackend is used for the storing generic secrets. These are not
// materialized in any way. The value that is written to this backend
// is the same value that is always returned. Leasing can be configured on
// a per-key basis.
type GenericBackend struct{}
// newGenericBackend is a factory constructor for the generic backend
func newGenericBackend(map[string]string) (vault.LogicalBackend, error) {
b := &GenericBackend{}
return b, nil
}
// HandleRequest is used to handle a request and generate a response.
// The backends must check the operation type and handle appropriately.
func (g *GenericBackend) HandleRequest(req *vault.Request) (*vault.Response, error) {
switch req.Operation {
case vault.ReadOperation:
return g.handleRead(req)
case vault.WriteOperation:
return g.handleWrite(req)
case vault.ListOperation:
return g.handleList(req)
case vault.DeleteOperation:
return g.handleDelete(req)
case vault.HelpOperation:
return g.handleHelp(req)
default:
return nil, vault.ErrUnsupportedOperation
}
}
// RootPaths is a list of paths that require root level privileges,
// which do not exist for the geneirc backend.
func (g *GenericBackend) RootPaths() []string {
return nil
}
func (g *GenericBackend) handleRead(req *vault.Request) (*vault.Response, error) {
// Read the path
out, err := req.View.Get(req.Path)
if err != nil {
return nil, fmt.Errorf("read failed: %v", err)
}
// Fast-path the no data case
if out == nil {
return nil, nil
}
// Decode the data
var raw map[string]interface{}
if err := json.Unmarshal(out.Value, &raw); err != nil {
return nil, fmt.Errorf("json decoding failed: %v", err)
}
// Check if there is a lease key
leaseVal, ok := raw["lease"].(string)
var lease *vault.Lease
if ok {
leaseDuration, err := time.ParseDuration(leaseVal)
if err == nil {
lease = &vault.Lease{
Renewable: false,
Revokable: false,
Duration: leaseDuration,
MaxDuration: leaseDuration,
MaxIncrement: 0,
}
}
}
// Generate the response
resp := &vault.Response{
IsSecret: true,
Lease: lease,
Data: raw,
}
return resp, nil
}
func (g *GenericBackend) handleWrite(req *vault.Request) (*vault.Response, error) {
// Check that some fields are given
if len(req.Data) == 0 {
return nil, fmt.Errorf("missing data fields")
}
// JSON encode the data
buf, err := json.Marshal(req.Data)
if err != nil {
return nil, fmt.Errorf("json encoding failed: %v", err)
}
// Write out a new key
entry := &vault.Entry{
Key: req.Path,
Value: buf,
}
if err := req.View.Put(entry); err != nil {
return nil, fmt.Errorf("failed to write: %v", err)
}
return nil, nil
}
func (g *GenericBackend) handleDelete(req *vault.Request) (*vault.Response, error) {
// Delete the key at the request path
if err := req.View.Delete(req.Path); err != nil {
return nil, err
}
return nil, nil
}
func (g *GenericBackend) handleList(req *vault.Request) (*vault.Response, error) {
// List the keys at the prefix given by the request
keys, err := req.View.List(req.Path)
if err != nil {
return nil, err
}
// Generate the response
resp := &vault.Response{
IsSecret: false,
Lease: nil,
Data: map[string]interface{}{
"keys": keys,
},
}
return resp, nil
}
func (g *GenericBackend) handleHelp(req *vault.Request) (*vault.Response, error) {
resp := &vault.Response{
IsSecret: false,
Lease: nil,
Data: map[string]interface{}{
"help": genericHelpText,
},
}
return resp, nil
}
// genericHelpText is the help information we return
const genericHelpText = "Generic backend for storing and retreiving raw keys with user-defined fields"

223
logical/generic_test.go Normal file
View File

@ -0,0 +1,223 @@
package logical
import (
"testing"
"time"
"github.com/hashicorp/vault/physical"
"github.com/hashicorp/vault/vault"
)
// mockRequest returns a request with a real view attached
func mockRequest(t *testing.T, op vault.Operation, path string) *vault.Request {
inm := physical.NewInmem()
b, err := vault.NewAESGCMBarrier(inm)
if err != nil {
t.Fatalf("err: %v", err)
}
// Initialize and unseal
key, _ := b.GenerateKey()
b.Initialize(key)
b.Unseal(key)
// Create the barrier view
view := vault.NewBarrierView(b, "logical/")
// Create the request
req := &vault.Request{
Operation: op,
Path: path,
Data: make(map[string]interface{}),
View: view,
}
return req
}
func TestGenericBackend_RootPaths(t *testing.T) {
b, err := newGenericBackend(nil)
if err != nil {
t.Fatalf("err: %v", err)
}
root := b.RootPaths()
if len(root) != 0 {
t.Fatalf("unexpected: %v", root)
}
}
func TestGenericBackend_Write(t *testing.T) {
b, err := newGenericBackend(nil)
if err != nil {
t.Fatalf("err: %v", err)
}
req := mockRequest(t, vault.WriteOperation, "foo")
req.Data["raw"] = "test"
resp, err := b.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp != nil {
t.Fatalf("bad: %v", resp)
}
out, err := req.View.Get("foo")
if err != nil {
t.Fatalf("err: %v", err)
}
if out == nil {
t.Fatalf("failed to write to view")
}
}
func TestGenericBackend_Read(t *testing.T) {
b, err := newGenericBackend(nil)
if err != nil {
t.Fatalf("err: %v", err)
}
req := mockRequest(t, vault.WriteOperation, "foo")
req.Data["raw"] = "test"
req.Data["lease"] = "1h"
if _, err := b.HandleRequest(req); err != nil {
t.Fatalf("err: %v", err)
}
req2 := mockRequest(t, vault.ReadOperation, "foo")
req2.View = req.View
resp, err := b.HandleRequest(req2)
if err != nil {
t.Fatalf("err: %v", err)
}
if !resp.IsSecret {
t.Fatalf("should be secret: %#v", resp)
}
if resp.Lease == nil {
t.Fatalf("should have lease: %#v", resp)
}
if resp.Lease.Renewable {
t.Fatalf("bad lease: %#v", resp.Lease)
}
if resp.Lease.Revokable {
t.Fatalf("bad lease: %#v", resp.Lease)
}
if resp.Lease.Duration != time.Hour {
t.Fatalf("bad lease: %#v", resp.Lease)
}
if resp.Lease.MaxDuration != time.Hour {
t.Fatalf("bad lease: %#v", resp.Lease)
}
if resp.Lease.MaxIncrement != 0 {
t.Fatalf("bad lease: %#v", resp.Lease)
}
if resp.Data["raw"] != "test" {
t.Fatalf("bad data: %#v", resp.Data)
}
if resp.Data["lease"] != "1h" {
t.Fatalf("bad data: %#v", resp.Data)
}
}
func TestGenericBackend_Delete(t *testing.T) {
b, err := newGenericBackend(nil)
if err != nil {
t.Fatalf("err: %v", err)
}
req := mockRequest(t, vault.WriteOperation, "foo")
req.Data["raw"] = "test"
req.Data["lease"] = "1h"
if _, err := b.HandleRequest(req); err != nil {
t.Fatalf("err: %v", err)
}
req2 := mockRequest(t, vault.DeleteOperation, "foo")
req2.View = req.View
resp, err := b.HandleRequest(req2)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp != nil {
t.Fatalf("bad: %v", resp)
}
req3 := mockRequest(t, vault.ReadOperation, "foo")
req3.View = req.View
resp, err = b.HandleRequest(req3)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp != nil {
t.Fatalf("bad: %v", resp)
}
}
func TestGenericBackend_List(t *testing.T) {
b, err := newGenericBackend(nil)
if err != nil {
t.Fatalf("err: %v", err)
}
req := mockRequest(t, vault.WriteOperation, "foo/bar")
req.Data["raw"] = "test"
req.Data["lease"] = "1h"
if _, err := b.HandleRequest(req); err != nil {
t.Fatalf("err: %v", err)
}
req2 := mockRequest(t, vault.ListOperation, "")
req2.View = req.View
resp, err := b.HandleRequest(req2)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp.IsSecret {
t.Fatalf("bad: %v", resp)
}
if resp.Lease != nil {
t.Fatalf("bad: %v", resp)
}
if resp.Data["keys"] == nil {
t.Fatalf("bad: %v", resp)
}
keys := resp.Data["keys"].([]string)
if len(keys) != 1 || keys[0] != "foo/" {
t.Fatalf("keys: %v", keys)
}
}
func TestGenericBackend_Help(t *testing.T) {
b, err := newGenericBackend(nil)
if err != nil {
t.Fatalf("err: %v", err)
}
req := mockRequest(t, vault.HelpOperation, "foo")
resp, err := b.HandleRequest(req)
if err != nil {
t.Fatalf("err: %v", err)
}
if resp.IsSecret {
t.Fatalf("bad: %v", resp)
}
if resp.Lease != nil {
t.Fatalf("bad: %v", resp)
}
if resp.Data["help"] != genericHelpText {
t.Fatalf("bad: %v", resp)
}
}

View File

@ -1,10 +1,17 @@
package vault
import (
"errors"
"fmt"
"time"
)
var (
// ErrUnsupportedOperation is returned if the operation is not supported
// by the logical backend.
ErrUnsupportedOperation = errors.New("unsupported operation")
)
// LogicalBackend interface must be implemented to be "mountable" at
// a given path. Requests flow through a router which has various mount
// points that flow to a logical backend. The logic of each backend is flexible,
@ -34,6 +41,7 @@ const (
WriteOperation = "write"
DeleteOperation = "delete"
ListOperation = "list"
RevokeOperation = "revoke"
HelpOperation = "help"
)
@ -50,6 +58,9 @@ type Request struct {
// final path is "foo" since the mount prefix is trimmed.
Path string
// Request data is an opaque map that must have string keys.
Data map[string]interface{}
// View is the storage view of this logical backend. It can be used
// to durably store and retrieve state from the backend.
View *BarrierView
@ -73,6 +84,7 @@ type Response struct {
type Lease struct {
VaultID string // VaultID is the unique identifier used for renewal and revocation
Renewable bool // Is the VaultID renewable
Revokable bool // Is the secret revokable. Must support 'Revoke' operation.
Duration time.Duration // Current lease duration
MaxDuration time.Duration // Maximum lease duration
MaxIncrement time.Duration // Maximum increment to lease duration