From 2120235a2ea505e1bfa01bff13c9438da6a62a6c Mon Sep 17 00:00:00 2001 From: Mitchell Hashimoto Date: Sat, 18 Apr 2015 18:37:27 -0700 Subject: [PATCH] logical/postgresql: create DB credentials --- builtin/logical/postgresql/backend.go | 5 + builtin/logical/postgresql/backend_test.go | 27 +++++- .../logical/postgresql/path_role_create.go | 95 +++++++++++++++++++ builtin/logical/postgresql/secret_creds.go | 59 ++++++++++++ builtin/logical/postgresql/uuid.go | 21 ++++ 5 files changed, 204 insertions(+), 3 deletions(-) create mode 100644 builtin/logical/postgresql/path_role_create.go create mode 100644 builtin/logical/postgresql/secret_creds.go create mode 100644 builtin/logical/postgresql/uuid.go diff --git a/builtin/logical/postgresql/backend.go b/builtin/logical/postgresql/backend.go index 65cd2230c..75c2c315f 100644 --- a/builtin/logical/postgresql/backend.go +++ b/builtin/logical/postgresql/backend.go @@ -28,6 +28,11 @@ func Backend() *framework.Backend { Paths: []*framework.Path{ pathConfigConnection(&b), pathRoles(&b), + pathRoleCreate(&b), + }, + + Secrets: []*framework.Secret{ + secretCreds(&b), }, } diff --git a/builtin/logical/postgresql/backend_test.go b/builtin/logical/postgresql/backend_test.go index 909279715..521c65138 100644 --- a/builtin/logical/postgresql/backend_test.go +++ b/builtin/logical/postgresql/backend_test.go @@ -1,11 +1,13 @@ package postgresql import ( + "log" "os" "testing" "github.com/hashicorp/vault/logical" logicaltest "github.com/hashicorp/vault/logical/testing" + "github.com/mitchellh/mapstructure" ) func TestBackend_basic(t *testing.T) { @@ -15,6 +17,7 @@ func TestBackend_basic(t *testing.T) { Steps: []logicaltest.TestStep{ testAccStepConfig(t), testAccStepRole(t), + testAccStepReadCreds(t, "web"), }, }) } @@ -45,9 +48,27 @@ func testAccStepRole(t *testing.T) logicaltest.TestStep { } } +func testAccStepReadCreds(t *testing.T, name string) logicaltest.TestStep { + return logicaltest.TestStep{ + Operation: logical.ReadOperation, + Path: name, + Check: func(resp *logical.Response) error { + var d struct { + Username string `mapstructure:"username"` + Password string `mapstructure:"password"` + } + if err := mapstructure.Decode(resp.Data, &d); err != nil { + return err + } + log.Printf("[WARN] Generated credentials: %v", d) + + return nil + }, + } +} + const testRole = ` -CREATE ROLE {{name}} WITH +CREATE ROLE "{{name}}" WITH LOGIN - PASSWORD '{{password}}' - VALID UNTIL '{{expiration}}'; + PASSWORD '{{password}}'; ` diff --git a/builtin/logical/postgresql/path_role_create.go b/builtin/logical/postgresql/path_role_create.go new file mode 100644 index 000000000..9feefcab2 --- /dev/null +++ b/builtin/logical/postgresql/path_role_create.go @@ -0,0 +1,95 @@ +package postgresql + +import ( + "fmt" + "math/rand" + "time" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + _ "github.com/lib/pq" +) + +func pathRoleCreate(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 role.", + }, + }, + + Callbacks: map[logical.Operation]framework.OperationFunc{ + logical.ReadOperation: b.pathRoleCreateRead, + }, + + HelpSynopsis: pathRoleCreateReadHelpSyn, + HelpDescription: pathRoleCreateReadHelpDesc, + } +} + +func (b *backend) pathRoleCreateRead( + req *logical.Request, data *framework.FieldData) (*logical.Response, error) { + name := data.Get("name").(string) + + // Get the role + entry, err := req.Storage.Get("role/" + name) + if err != nil { + return nil, err + } + if entry == nil { + return logical.ErrorResponse(fmt.Sprintf("unknown role: %s", name)), nil + } + var role struct { + SQL string `json:"sql"` + } + if err := entry.DecodeJSON(&role); err != nil { + return nil, err + } + + // Get our connection + db, err := b.DB(req.Storage) + if err != nil { + return nil, err + } + + // Generate our query + username := fmt.Sprintf( + "vault-%s-%d-%d", + req.DisplayName, time.Now().Unix(), rand.Int31n(10000)) + password := generateUUID() + query := Query(role.SQL, map[string]string{ + "name": username, + "password": password, + "expiration": "", + }) + + // Prepare the statement and execute it + stmt, err := db.Prepare(query) + if err != nil { + return nil, err + } + defer stmt.Close() + if _, err := stmt.Exec(); err != nil { + return nil, err + } + + // Return the secret + return b.Secret(SecretCredsType).Response(map[string]interface{}{ + "username": username, + "password": password, + }, map[string]interface{}{ + "username": username, + }), nil +} + +const pathRoleCreateReadHelpSyn = ` +Request database credentials for a certain role. +` + +const pathRoleCreateReadHelpDesc = ` +This path reads database credentials for a certain role. The +database credentials will be generated on demand and will be automatically +revoked when the lease is up. +` diff --git a/builtin/logical/postgresql/secret_creds.go b/builtin/logical/postgresql/secret_creds.go new file mode 100644 index 000000000..5ae78fe46 --- /dev/null +++ b/builtin/logical/postgresql/secret_creds.go @@ -0,0 +1,59 @@ +package postgresql + +import ( + "fmt" + + "github.com/hashicorp/vault/logical" + "github.com/hashicorp/vault/logical/framework" + "github.com/lib/pq" +) + +const SecretCredsType = "creds" + +func secretCreds(b *backend) *framework.Secret { + return &framework.Secret{ + Type: SecretCredsType, + Fields: map[string]*framework.FieldSchema{ + "username": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Username", + }, + + "password": &framework.FieldSchema{ + Type: framework.TypeString, + Description: "Password", + }, + }, + + Revoke: b.secretCredsRevoke, + } +} + +func (b *backend) secretCredsRevoke( + req *logical.Request, d *framework.FieldData) (*logical.Response, error) { + // Get the username from the internal data + usernameRaw, ok := req.Secret.InternalData["username"] + if !ok { + return nil, fmt.Errorf("secret is missing username internal data") + } + username, ok := usernameRaw.(string) + + // Get our connection + db, err := b.DB(req.Storage) + if err != nil { + return nil, err + } + + // Drop this user + stmt, err := db.Prepare(fmt.Sprintf( + "DROP ROLE IF EXISTS %s;", pq.QuoteIdentifier(username))) + if err != nil { + return nil, err + } + defer stmt.Close() + if _, err := stmt.Exec(); err != nil { + return nil, err + } + + return nil, nil +} diff --git a/builtin/logical/postgresql/uuid.go b/builtin/logical/postgresql/uuid.go new file mode 100644 index 000000000..8e3dd66bc --- /dev/null +++ b/builtin/logical/postgresql/uuid.go @@ -0,0 +1,21 @@ +package postgresql + +import ( + "crypto/rand" + "fmt" +) + +// generateUUID is used to generate a random UUID +func generateUUID() string { + buf := make([]byte, 16) + if _, err := rand.Read(buf); err != nil { + panic(fmt.Errorf("failed to read random bytes: %v", err)) + } + + return fmt.Sprintf("%08x-%04x-%04x-%04x-%12x", + buf[0:4], + buf[4:6], + buf[6:8], + buf[8:10], + buf[10:16]) +}