80dc5819d3
New dockertest has a totally different API and will require some serious refactoring. This will tide over until then by pinning the API version.
369 lines
8.6 KiB
Go
369 lines
8.6 KiB
Go
package mysql
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"reflect"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/vault/logical"
|
|
logicaltest "github.com/hashicorp/vault/logical/testing"
|
|
"github.com/mitchellh/mapstructure"
|
|
dockertest "gopkg.in/ory-am/dockertest.v2"
|
|
)
|
|
|
|
var (
|
|
testImagePull sync.Once
|
|
)
|
|
|
|
func prepareTestContainer(t *testing.T, s logical.Storage, b logical.Backend) (cid dockertest.ContainerID, retURL string) {
|
|
if os.Getenv("MYSQL_DSN") != "" {
|
|
return "", os.Getenv("MYSQL_DSN")
|
|
}
|
|
|
|
// Without this the checks for whether the container has started seem to
|
|
// never actually pass. There's really no reason to expose the test
|
|
// containers, so don't.
|
|
dockertest.BindDockerToLocalhost = "yep"
|
|
|
|
testImagePull.Do(func() {
|
|
dockertest.Pull("mysql")
|
|
})
|
|
|
|
cid, connErr := dockertest.ConnectToMySQL(60, 500*time.Millisecond, func(connURL string) bool {
|
|
// This will cause a validation to run
|
|
resp, err := b.HandleRequest(&logical.Request{
|
|
Storage: s,
|
|
Operation: logical.UpdateOperation,
|
|
Path: "config/connection",
|
|
Data: map[string]interface{}{
|
|
"connection_url": connURL,
|
|
},
|
|
})
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
// It's likely not up and running yet, so return false and try again
|
|
return false
|
|
}
|
|
if resp == nil {
|
|
t.Fatal("expected warning")
|
|
}
|
|
|
|
retURL = connURL
|
|
return true
|
|
})
|
|
|
|
if connErr != nil {
|
|
t.Fatalf("could not connect to database: %v", connErr)
|
|
}
|
|
|
|
return
|
|
}
|
|
|
|
func cleanupTestContainer(t *testing.T, cid dockertest.ContainerID) {
|
|
err := cid.KillRemove()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func TestBackend_config_connection(t *testing.T) {
|
|
var resp *logical.Response
|
|
var err error
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
b, err := Factory(config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
configData := map[string]interface{}{
|
|
"value": "",
|
|
"connection_url": "sample_connection_url",
|
|
"max_open_connections": 9,
|
|
"max_idle_connections": 7,
|
|
"verify_connection": false,
|
|
}
|
|
|
|
configReq := &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "config/connection",
|
|
Storage: config.StorageView,
|
|
Data: configData,
|
|
}
|
|
resp, err = b.HandleRequest(configReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
|
}
|
|
|
|
configReq.Operation = logical.ReadOperation
|
|
resp, err = b.HandleRequest(configReq)
|
|
if err != nil || (resp != nil && resp.IsError()) {
|
|
t.Fatalf("err:%s resp:%#v\n", err, resp)
|
|
}
|
|
|
|
delete(configData, "verify_connection")
|
|
if !reflect.DeepEqual(configData, resp.Data) {
|
|
t.Fatalf("bad: expected:%#v\nactual:%#v\n", configData, resp.Data)
|
|
}
|
|
}
|
|
|
|
func TestBackend_basic(t *testing.T) {
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
b, err := Factory(config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cid, connURL := prepareTestContainer(t, config.StorageView, b)
|
|
if cid != "" {
|
|
defer cleanupTestContainer(t, cid)
|
|
}
|
|
connData := map[string]interface{}{
|
|
"connection_url": connURL,
|
|
}
|
|
|
|
// for wildcard based mysql user
|
|
logicaltest.Test(t, logicaltest.TestCase{
|
|
Backend: b,
|
|
Steps: []logicaltest.TestStep{
|
|
testAccStepConfig(t, connData, false),
|
|
testAccStepRole(t, true),
|
|
testAccStepReadCreds(t, "web"),
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestBackend_basicHostRevoke(t *testing.T) {
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
b, err := Factory(config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cid, connURL := prepareTestContainer(t, config.StorageView, b)
|
|
if cid != "" {
|
|
defer cleanupTestContainer(t, cid)
|
|
}
|
|
connData := map[string]interface{}{
|
|
"connection_url": connURL,
|
|
}
|
|
|
|
// for host based mysql user
|
|
logicaltest.Test(t, logicaltest.TestCase{
|
|
Backend: b,
|
|
Steps: []logicaltest.TestStep{
|
|
testAccStepConfig(t, connData, false),
|
|
testAccStepRole(t, false),
|
|
testAccStepReadCreds(t, "web"),
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestBackend_roleCrud(t *testing.T) {
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
b, err := Factory(config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cid, connURL := prepareTestContainer(t, config.StorageView, b)
|
|
if cid != "" {
|
|
defer cleanupTestContainer(t, cid)
|
|
}
|
|
connData := map[string]interface{}{
|
|
"connection_url": connURL,
|
|
}
|
|
|
|
logicaltest.Test(t, logicaltest.TestCase{
|
|
Backend: b,
|
|
Steps: []logicaltest.TestStep{
|
|
testAccStepConfig(t, connData, false),
|
|
// test SQL with wildcard based user
|
|
testAccStepRole(t, true),
|
|
testAccStepReadRole(t, "web", testRoleWildCard),
|
|
testAccStepDeleteRole(t, "web"),
|
|
// test SQL with host based user
|
|
testAccStepRole(t, false),
|
|
testAccStepReadRole(t, "web", testRoleHost),
|
|
testAccStepDeleteRole(t, "web"),
|
|
},
|
|
})
|
|
}
|
|
|
|
func TestBackend_leaseWriteRead(t *testing.T) {
|
|
config := logical.TestBackendConfig()
|
|
config.StorageView = &logical.InmemStorage{}
|
|
b, err := Factory(config)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
cid, connURL := prepareTestContainer(t, config.StorageView, b)
|
|
if cid != "" {
|
|
defer cleanupTestContainer(t, cid)
|
|
}
|
|
connData := map[string]interface{}{
|
|
"connection_url": connURL,
|
|
}
|
|
|
|
logicaltest.Test(t, logicaltest.TestCase{
|
|
Backend: b,
|
|
Steps: []logicaltest.TestStep{
|
|
testAccStepConfig(t, connData, false),
|
|
testAccStepWriteLease(t),
|
|
testAccStepReadLease(t),
|
|
},
|
|
})
|
|
|
|
}
|
|
|
|
func testAccStepConfig(t *testing.T, d map[string]interface{}, expectError bool) logicaltest.TestStep {
|
|
return logicaltest.TestStep{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "config/connection",
|
|
Data: d,
|
|
ErrorOk: true,
|
|
Check: func(resp *logical.Response) error {
|
|
if expectError {
|
|
if resp.Data == nil {
|
|
return fmt.Errorf("data is nil")
|
|
}
|
|
var e struct {
|
|
Error string `mapstructure:"error"`
|
|
}
|
|
if err := mapstructure.Decode(resp.Data, &e); err != nil {
|
|
return err
|
|
}
|
|
if len(e.Error) == 0 {
|
|
return fmt.Errorf("expected error, but write succeeded")
|
|
}
|
|
return nil
|
|
} else if resp != nil && resp.IsError() {
|
|
return fmt.Errorf("got an error response: %v", resp.Error())
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
}
|
|
|
|
func testAccStepRole(t *testing.T, wildCard bool) logicaltest.TestStep {
|
|
|
|
pathData := make(map[string]interface{})
|
|
if wildCard == true {
|
|
pathData = map[string]interface{}{
|
|
"sql": testRoleWildCard,
|
|
}
|
|
} else {
|
|
pathData = map[string]interface{}{
|
|
"sql": testRoleHost,
|
|
"revocation_sql": testRevocationSQL,
|
|
}
|
|
}
|
|
|
|
return logicaltest.TestStep{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "roles/web",
|
|
Data: pathData,
|
|
}
|
|
|
|
}
|
|
|
|
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 string, 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 testRoleWildCard = `
|
|
CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';
|
|
GRANT SELECT ON *.* TO '{{name}}'@'%';
|
|
`
|
|
const testRoleHost = `
|
|
CREATE USER '{{name}}'@'10.1.1.2' IDENTIFIED BY '{{password}}';
|
|
GRANT SELECT ON *.* TO '{{name}}'@'10.1.1.2';
|
|
`
|
|
const testRevocationSQL = `
|
|
REVOKE ALL PRIVILEGES, GRANT OPTION FROM '{{name}}'@'10.1.1.2';
|
|
DROP USER '{{name}}'@'10.1.1.2';
|
|
`
|