logical/consul

This commit is contained in:
Mitchell Hashimoto 2015-03-21 17:19:37 +01:00
parent 26583fb05f
commit 07f8e262fe
7 changed files with 421 additions and 0 deletions

View File

@ -0,0 +1,36 @@
package consul
import (
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func Factory(map[string]string) (logical.Backend, error) {
return Backend(), nil
}
func Backend() *framework.Backend {
var b backend
b.Backend = &framework.Backend{
PathsRoot: []string{
"config",
"policy/*",
},
Paths: []*framework.Path{
pathConfig(),
pathPolicy(),
pathToken(&b),
},
Secrets: []*framework.Secret{
secretToken(),
},
}
return b.Backend
}
type backend struct {
*framework.Backend
}

View File

@ -0,0 +1,139 @@
package consul
import (
"encoding/base64"
"io/ioutil"
"log"
"os"
"os/exec"
"strings"
"testing"
"time"
"github.com/hashicorp/vault/logical"
logicaltest "github.com/hashicorp/vault/logical/testing"
"github.com/mitchellh/mapstructure"
)
func TestBackend_basic(t *testing.T) {
config, process := testStartConsulServer(t)
defer testStopConsulServer(t, process)
logicaltest.Test(t, logicaltest.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Backend: Backend(),
Steps: []logicaltest.TestStep{
testAccStepConfig(t, config),
testAccStepWritePolicy(t, "test", testPolicy),
testAccStepReadToken(t, "test"),
},
})
}
func testStartConsulServer(t *testing.T) (map[string]interface{}, *os.Process) {
td, err := ioutil.TempDir("", "vault")
if err != nil {
t.Fatalf("err: %s", err)
}
tf, err := ioutil.TempFile("", "vault")
if err != nil {
t.Fatalf("err: %s", err)
}
if _, err := tf.Write([]byte(strings.TrimSpace(testConsulConfig))); err != nil {
t.Fatalf("err: %s", err)
}
tf.Close()
cmd := exec.Command(
"consul", "agent",
"-server",
"-bootstrap",
"-config-file", tf.Name(),
"-data-dir", td)
if err := cmd.Start(); err != nil {
t.Fatalf("error starting Consul: %s", err)
}
// Give Consul time to startup
time.Sleep(2 * time.Second)
config := map[string]interface{}{
"address": "127.0.0.1:8500",
"token": "test",
}
return config, cmd.Process
}
func testStopConsulServer(t *testing.T, p *os.Process) {
p.Kill()
}
func testAccPreCheck(t *testing.T) {
if _, err := exec.LookPath("consul"); err != nil {
t.Fatal("consul must be on PATH")
}
}
func testAccStepConfig(
t *testing.T, config map[string]interface{}) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.WriteOperation,
Path: "config",
Data: config,
}
}
func testAccStepReadToken(t *testing.T, name string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: name,
Check: func(resp *logical.Response) error {
var d struct {
Token string `mapstructure:"token"`
}
if err := mapstructure.Decode(resp.Data, &d); err != nil {
return err
}
log.Printf("[WARN] Generated token: %s", d.Token)
/*
// Build a client and verify that the credentials work
creds := aws.Creds(d.AccessKey, d.SecretKey, "")
client := ec2.New(creds, "us-east-1", nil)
log.Printf("[WARN] Verifying that the generated credentials work...")
_, err := client.DescribeInstances(&ec2.DescribeInstancesRequest{})
if err != nil {
return err
}
*/
return nil
},
}
}
func testAccStepWritePolicy(t *testing.T, name string, policy string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.WriteOperation,
Path: "policy/" + name,
Data: map[string]interface{}{
"policy": base64.StdEncoding.EncodeToString([]byte(policy)),
},
}
}
const testPolicy = `
key "" {
policy = "write"
}
`
const testConsulConfig = `
{
"datacenter": "test",
"acl_datacenter": "test",
"acl_master_token": "test"
}
`

View File

@ -0,0 +1,32 @@
package consul
import (
"fmt"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/vault/logical"
)
func client(s logical.Storage) (*api.Client, error) {
entry, err := s.Get("config")
if err != nil {
return nil, err
}
if entry == nil {
return nil, fmt.Errorf(
"root credentials haven't been configured. Please configure\n" +
"them at the '/root' endpoint")
}
var conf config
if err := entry.DecodeJSON(&conf); err != nil {
return nil, fmt.Errorf("error reading root configuration: %s", err)
}
consulConf := api.DefaultConfig()
consulConf.Address = conf.Address
consulConf.Scheme = conf.Scheme
consulConf.Token = conf.Token
return api.NewClient(consulConf)
}

View File

@ -0,0 +1,61 @@
package consul
import (
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathConfig() *framework.Path {
return &framework.Path{
Pattern: "config",
Fields: map[string]*framework.FieldSchema{
"address": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Consul server address",
},
"scheme": &framework.FieldSchema{
Type: framework.TypeString,
Description: "URI scheme for the Consul address",
// https would be a better default but Consul on its own
// defaults to HTTP access, and when HTTPS is enabled it
// disables HTTP, so there isn't really any harm done here.
Default: "http",
},
"token": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Token for API calls",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.WriteOperation: pathConfigWrite,
},
}
}
func pathConfigWrite(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
entry, err := logical.StorageEntryJSON("config", config{
Address: data.Get("address").(string),
Scheme: data.Get("scheme").(string),
Token: data.Get("token").(string),
})
if err != nil {
return nil, err
}
if err := req.Storage.Put(entry); err != nil {
return nil, err
}
return nil, nil
}
type config struct {
Address string `json:"address"`
Scheme string `json:"scheme"`
Token string `json:"token"`
}

View File

@ -0,0 +1,50 @@
package consul
import (
"encoding/base64"
"fmt"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathPolicy() *framework.Path {
return &framework.Path{
Pattern: `policy/(?P<name>\w+)`,
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the policy",
},
"policy": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Policy document, base64 encoded.",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.WriteOperation: pathPolicyWrite,
},
}
}
func pathPolicyWrite(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
policyRaw, err := base64.StdEncoding.DecodeString(d.Get("policy").(string))
if err != nil {
return logical.ErrorResponse(fmt.Sprintf(
"Error decoding policy base64: %s", err)), nil
}
// Write the policy into storage
err = req.Storage.Put(&logical.StorageEntry{
Key: "policy/" + d.Get("name").(string),
Value: policyRaw,
})
if err != nil {
return nil, err
}
return nil, nil
}

View File

@ -0,0 +1,66 @@
package consul
import (
"fmt"
"math/rand"
"time"
"github.com/hashicorp/consul/api"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
func pathToken(b *backend) *framework.Path {
return &framework.Path{
Pattern: `(?P<name>\w+)`,
Fields: map[string]*framework.FieldSchema{
"name": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Name of the policy",
},
},
Callbacks: map[logical.Operation]framework.OperationFunc{
logical.ReadOperation: b.pathTokenRead,
},
}
}
func (b *backend) pathTokenRead(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
policyName := d.Get("name").(string)
// Generate a random name for the token
name := fmt.Sprintf("vault-%d-%d", time.Now().Unix(), rand.Int31n(10000))
// Read the policy
policy, err := req.Storage.Get("policy/" + policyName)
if err != nil {
return nil, fmt.Errorf("error retrieving policy: %s", err)
}
if policy == nil {
return logical.ErrorResponse(fmt.Sprintf(
"Policy '%s' not found", policyName)), nil
}
// Get the consul client
c, err := client(req.Storage)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
// Create it
token, _, err := c.ACL().Create(&api.ACLEntry{
Name: name,
Type: "client",
Rules: string(policy.Value),
}, nil)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
// Use the helper to create the secret
return b.Secret(SecretTokenType).Response(map[string]interface{}{
"token": token,
}, nil), nil
}

View File

@ -0,0 +1,37 @@
package consul
import (
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)
const SecretTokenType = "token"
func secretToken() *framework.Secret {
return &framework.Secret{
Type: SecretTokenType,
Fields: map[string]*framework.FieldSchema{
"token": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Request token",
},
},
Revoke: secretTokenRevoke,
}
}
func secretTokenRevoke(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
c, err := client(req.Storage)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
_, err = c.ACL().Destroy(d.Get("token").(string), nil)
if err != nil {
return logical.ErrorResponse(err.Error()), nil
}
return nil, nil
}