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
Changes from 2 commits
ed5ca17
ba34a1b
1d7fe31
7fa20ef
bfa943c
e6ce216
b9c8f95
41b5847
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
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{ | ||
"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 | ||
} | ||
conn := connConfig.ConnectionParams | ||
|
||
b.db, err = sql.Open("mssql", BuildDsn(conn)) | ||
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. | ||
b.db.SetMaxOpenConns(connConfig.MaxOpenConnections) | ||
|
||
stmt, err := b.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 | ||
} | ||
|
||
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) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Missing a period at the end! |
||
` |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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_PARAMS"); v == "" { | ||
t.Fatal("MSSQL_PARAMS 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_params": os.Getenv("MSSQL_PARAMS"), | ||
}, | ||
} | ||
} | ||
|
||
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}}] | ||
` |
There was a problem hiding this comment.
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.