Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Sql Server (mssql) secret backend #998

Merged
merged 8 commits into from Mar 11, 2016
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
11 changes: 8 additions & 3 deletions Godeps/Godeps.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

139 changes: 139 additions & 0 deletions builtin/logical/mssql/backend.go
@@ -0,0 +1,139 @@
package mssql

import (
"database/sql"
"fmt"
"strings"
"sync"

_ "github.com/denisenkom/go-mssqldb"
"github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework"
)

func Factory(conf *logical.BackendConfig) (logical.Backend, error) {
return Backend().Setup(conf)
}

func Backend() *framework.Backend {
var b backend
b.Backend = &framework.Backend{
Help: strings.TrimSpace(backendHelp),

PathsSpecial: &logical.Paths{
Root: []string{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please remove this from being a Root path -- we're generally trying to get away from having them outside of specific paths in /sys, since the normal ACL system is sufficient.

"config/*",
},
},

Paths: []*framework.Path{
pathConfigConnection(&b),
pathConfigLease(&b),
pathListRoles(&b),
pathRoles(&b),
pathCredsCreate(&b),
},

Secrets: []*framework.Secret{
secretCreds(&b),
},
}

return b.Backend
}

type backend struct {
*framework.Backend

db *sql.DB
defaultDb string
lock sync.Mutex
}

// DB returns the default database connection.
func (b *backend) DB(s logical.Storage) (*sql.DB, error) {
b.lock.Lock()
defer b.lock.Unlock()

// If we already have a DB, we got it!
if b.db != nil {
return b.db, nil
}

// Otherwise, attempt to make connection
entry, err := s.Get("config/connection")
if err != nil {
return nil, err
}
if entry == nil {
return nil, fmt.Errorf("configure the DB connection with config/connection first")
}

var connConfig connectionConfig
if err := entry.DecodeJSON(&connConfig); err != nil {
return nil, err
}
connString := connConfig.ConnectionString

db, err := sql.Open("mssql", connString)
if err != nil {
return nil, err
}

// Set some connection pool settings. We don't need much of this,
// since the request rate shouldn't be high.
db.SetMaxOpenConns(connConfig.MaxOpenConnections)

stmt, err := db.Prepare("SELECT db_name();")
if err != nil {
return nil, err
}
defer stmt.Close()

err = stmt.QueryRow().Scan(&b.defaultDb)
if err != nil {
return nil, err
}

b.db = db
return b.db, nil
}

// ResetDB forces a connection next time DB() is called.
func (b *backend) ResetDB() {
b.lock.Lock()
defer b.lock.Unlock()

if b.db != nil {
b.db.Close()
}

b.db = nil
}

// Lease returns the lease information
func (b *backend) Lease(s logical.Storage) (*configLease, error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please name this LeaseConfig for clarity.

entry, err := s.Get("config/lease")
if err != nil {
return nil, err
}
if entry == nil {
return nil, nil
}

var result configLease
if err := entry.DecodeJSON(&result); err != nil {
return nil, err
}

return &result, nil
}

const backendHelp = `
The MSSQL backend dynamically generates database users.

After mounting this backend, configure it using the endpoints within
the "config/" path.

This backend does not support Azure SQL Databases
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Missing a period at the end!

`
169 changes: 169 additions & 0 deletions builtin/logical/mssql/backend_test.go
@@ -0,0 +1,169 @@
package mssql

import (
"fmt"
"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) {
b, _ := Factory(logical.TestBackendConfig())

logicaltest.Test(t, logicaltest.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Backend: b,
Steps: []logicaltest.TestStep{
testAccStepConfig(t),
testAccStepRole(t),
testAccStepReadCreds(t, "web"),
},
})
}

func TestBackend_roleCrud(t *testing.T) {
b := Backend()

logicaltest.Test(t, logicaltest.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Backend: b,
Steps: []logicaltest.TestStep{
testAccStepConfig(t),
testAccStepRole(t),
testAccStepReadRole(t, "web", testRoleSQL),
testAccStepDeleteRole(t, "web"),
testAccStepReadRole(t, "web", ""),
},
})
}

func TestBackend_leaseWriteRead(t *testing.T) {
b := Backend()

logicaltest.Test(t, logicaltest.TestCase{
PreCheck: func() { testAccPreCheck(t) },
Backend: b,
Steps: []logicaltest.TestStep{
testAccStepConfig(t),
testAccStepWriteLease(t),
testAccStepReadLease(t),
},
})

}

func testAccPreCheck(t *testing.T) {
if v := os.Getenv("MSSQL_DSN"); v == "" {
t.Fatal("MSSQL_DSN must be set for acceptance tests")
}
}

func testAccStepConfig(t *testing.T) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "config/connection",
Data: map[string]interface{}{
"connection_string": os.Getenv("MSSQL_DSN"),
},
}
}

func testAccStepRole(t *testing.T) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "roles/web",
Data: map[string]interface{}{
"sql": testRoleSQL,
},
}
}

func testAccStepDeleteRole(t *testing.T, n string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.DeleteOperation,
Path: "roles/" + n,
}
}

func testAccStepReadCreds(t *testing.T, name string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "creds/" + 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
},
}
}

func testAccStepReadRole(t *testing.T, name, sql string) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "roles/" + name,
Check: func(resp *logical.Response) error {
if resp == nil {
if sql == "" {
return nil
}

return fmt.Errorf("bad: %#v", resp)
}

var d struct {
SQL string `mapstructure:"sql"`
}
if err := mapstructure.Decode(resp.Data, &d); err != nil {
return err
}

if d.SQL != sql {
return fmt.Errorf("bad: %#v", resp)
}

return nil
},
}
}

func testAccStepWriteLease(t *testing.T) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.UpdateOperation,
Path: "config/lease",
Data: map[string]interface{}{
"lease": "1h5m",
"lease_max": "24h",
},
}
}

func testAccStepReadLease(t *testing.T) logicaltest.TestStep {
return logicaltest.TestStep{
Operation: logical.ReadOperation,
Path: "config/lease",
Check: func(resp *logical.Response) error {
if resp.Data["lease"] != "1h5m0s" || resp.Data["lease_max"] != "24h0m0s" {
return fmt.Errorf("bad: %#v", resp)
}

return nil
},
}
}

const testRoleSQL = `
CREATE LOGIN [{{name}}] WITH PASSWORD = '{{password}}';
CREATE USER [{{name}}] FOR LOGIN [{{name}}];
GRANT SELECT ON SCHEMA::dbo TO [{{name}}]
`