From 07f8e262fecc91c924a0f22a013e46671e9528e8 Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 21 Mar 2015 17:19:37 +0100 Subject: [PATCH] logical/consul --- builtin/logical/consul/backend.go | 36 +++++++ builtin/logical/consul/backend_test.go | 139 +++++++++++++++++++++++++ builtin/logical/consul/client.go | 32 ++++++ builtin/logical/consul/path_config.go | 61 +++++++++++ builtin/logical/consul/path_policy.go | 50 +++++++++ builtin/logical/consul/path_token.go | 66 ++++++++++++ builtin/logical/consul/secret_token.go | 37 +++++++ 7 files changed, 421 insertions(+) create mode 100644 builtin/logical/consul/backend.go create mode 100644 builtin/logical/consul/backend_test.go create mode 100644 builtin/logical/consul/client.go create mode 100644 builtin/logical/consul/path_config.go create mode 100644 builtin/logical/consul/path_policy.go create mode 100644 builtin/logical/consul/path_token.go create mode 100644 builtin/logical/consul/secret_token.go diff --git a/builtin/logical/consul/backend.go b/builtin/logical/consul/backend.go new file mode 100644 index 0000000000..2071f4d5c3 --- /dev/null +++ b/builtin/logical/consul/backend.go @@ -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 +} diff --git a/builtin/logical/consul/backend_test.go b/builtin/logical/consul/backend_test.go new file mode 100644 index 0000000000..dd9e7e3052 --- /dev/null +++ b/builtin/logical/consul/backend_test.go @@ -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" +} +` diff --git a/builtin/logical/consul/client.go b/builtin/logical/consul/client.go new file mode 100644 index 0000000000..ee57d41760 --- /dev/null +++ b/builtin/logical/consul/client.go @@ -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) +} diff --git a/builtin/logical/consul/path_config.go b/builtin/logical/consul/path_config.go new file mode 100644 index 0000000000..1e816461ee --- /dev/null +++ b/builtin/logical/consul/path_config.go @@ -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"` +} diff --git a/builtin/logical/consul/path_policy.go b/builtin/logical/consul/path_policy.go new file mode 100644 index 0000000000..ca25550cd1 --- /dev/null +++ b/builtin/logical/consul/path_policy.go @@ -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\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 +} diff --git a/builtin/logical/consul/path_token.go b/builtin/logical/consul/path_token.go new file mode 100644 index 0000000000..c22b290d5c --- /dev/null +++ b/builtin/logical/consul/path_token.go @@ -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\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 +} diff --git a/builtin/logical/consul/secret_token.go b/builtin/logical/consul/secret_token.go new file mode 100644 index 0000000000..7bc763763b --- /dev/null +++ b/builtin/logical/consul/secret_token.go @@ -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 +}