Merge pull request #1914 from jpweber/mysql-revoke

Mysql revoke with non-wildcard hosts
This commit is contained in:
Vishal Nayak 2016-10-04 17:44:15 -04:00 committed by GitHub
commit 1ab7023483
4 changed files with 159 additions and 24 deletions

View file

@ -126,11 +126,39 @@ func TestBackend_basic(t *testing.T) {
"connection_url": connURL, "connection_url": connURL,
} }
// for wildcard based mysql user
logicaltest.Test(t, logicaltest.TestCase{ logicaltest.Test(t, logicaltest.TestCase{
Backend: b, Backend: b,
Steps: []logicaltest.TestStep{ Steps: []logicaltest.TestStep{
testAccStepConfig(t, connData, false), testAccStepConfig(t, connData, false),
testAccStepRole(t), 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"), testAccStepReadCreds(t, "web"),
}, },
}) })
@ -156,10 +184,14 @@ func TestBackend_roleCrud(t *testing.T) {
Backend: b, Backend: b,
Steps: []logicaltest.TestStep{ Steps: []logicaltest.TestStep{
testAccStepConfig(t, connData, false), testAccStepConfig(t, connData, false),
testAccStepRole(t), // test SQL with wildcard based user
testAccStepReadRole(t, "web", testRole), 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"), testAccStepDeleteRole(t, "web"),
testAccStepReadRole(t, "web", ""),
}, },
}) })
} }
@ -209,7 +241,7 @@ func testAccStepConfig(t *testing.T, d map[string]interface{}, expectError bool)
return err return err
} }
if len(e.Error) == 0 { if len(e.Error) == 0 {
return fmt.Errorf("expected error, but write succeeded.") return fmt.Errorf("expected error, but write succeeded")
} }
return nil return nil
} else if resp != nil && resp.IsError() { } else if resp != nil && resp.IsError() {
@ -220,14 +252,26 @@ func testAccStepConfig(t *testing.T, d map[string]interface{}, expectError bool)
} }
} }
func testAccStepRole(t *testing.T) logicaltest.TestStep { 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,
"revoke_sql": testRevokeSQL,
}
}
return logicaltest.TestStep{ return logicaltest.TestStep{
Operation: logical.UpdateOperation, Operation: logical.UpdateOperation,
Path: "roles/web", Path: "roles/web",
Data: map[string]interface{}{ Data: pathData,
"sql": testRole,
},
} }
} }
func testAccStepDeleteRole(t *testing.T, n string) logicaltest.TestStep { func testAccStepDeleteRole(t *testing.T, n string) logicaltest.TestStep {
@ -310,7 +354,15 @@ func testAccStepReadLease(t *testing.T) logicaltest.TestStep {
} }
} }
const testRole = ` const testRoleWildCard = `
CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'; CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}';
GRANT SELECT ON *.* TO '{{name}}'@'%'; 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 testRevokeSQL = `
REVOKE ALL PRIVILEGES, GRANT OPTION FROM '{{name}}'@'10.1.1.2';
DROP USER '{{name}}'@'10.1.1.2';
`

View file

@ -127,6 +127,7 @@ func (b *backend) pathRoleCreateRead(
"password": password, "password": password,
}, map[string]interface{}{ }, map[string]interface{}{
"username": username, "username": username,
"role": name,
}) })
resp.Secret.TTL = lease.Lease resp.Secret.TTL = lease.Lease
return resp, nil return resp, nil

View file

@ -37,6 +37,11 @@ func pathRoles(b *backend) *framework.Path {
Description: "SQL string to create a user. See help for more info.", Description: "SQL string to create a user. See help for more info.",
}, },
"revoke_sql": &framework.FieldSchema{
Type: framework.TypeString,
Description: "SQL string to revoke a user. See help for more info.",
},
"username_length": &framework.FieldSchema{ "username_length": &framework.FieldSchema{
Type: framework.TypeInt, Type: framework.TypeInt,
Description: "number of characters to truncate generated mysql usernames to (default 16)", Description: "number of characters to truncate generated mysql usernames to (default 16)",
@ -112,7 +117,8 @@ func (b *backend) pathRoleRead(
return &logical.Response{ return &logical.Response{
Data: map[string]interface{}{ Data: map[string]interface{}{
"sql": role.SQL, "sql": role.SQL,
"revoke_sql": role.RevokeSQL,
}, },
}, nil }, nil
} }
@ -131,6 +137,7 @@ func (b *backend) pathRoleCreate(
req *logical.Request, data *framework.FieldData) (*logical.Response, error) { req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
name := data.Get("name").(string) name := data.Get("name").(string)
sql := data.Get("sql").(string) sql := data.Get("sql").(string)
revoke_sql := data.Get("revoke_sql").(string)
username_length := data.Get("username_length").(int) username_length := data.Get("username_length").(int)
rolename_length := data.Get("rolename_length").(int) rolename_length := data.Get("rolename_length").(int)
displayname_length := data.Get("displayname_length").(int) displayname_length := data.Get("displayname_length").(int)
@ -162,6 +169,7 @@ func (b *backend) pathRoleCreate(
// Store it // Store it
entry, err := logical.StorageEntryJSON("role/"+name, &roleEntry{ entry, err := logical.StorageEntryJSON("role/"+name, &roleEntry{
SQL: sql, SQL: sql,
RevokeSQL: revoke_sql,
UsernameLength: username_length, UsernameLength: username_length,
DisplaynameLength: displayname_length, DisplaynameLength: displayname_length,
RolenameLength: rolename_length, RolenameLength: rolename_length,
@ -177,6 +185,7 @@ func (b *backend) pathRoleCreate(
type roleEntry struct { type roleEntry struct {
SQL string `json:"sql"` SQL string `json:"sql"`
RevokeSQL string `json:"revoke_sql"`
UsernameLength int `json:"username_length"` UsernameLength int `json:"username_length"`
DisplaynameLength int `json:"displayname_length"` DisplaynameLength int `json:"displayname_length"`
RolenameLength int `json:"rolename_length"` RolenameLength int `json:"rolename_length"`

View file

@ -2,13 +2,29 @@ package mysql
import ( import (
"fmt" "fmt"
"strings"
"github.com/hashicorp/vault/helper/strutil"
"github.com/hashicorp/vault/logical" "github.com/hashicorp/vault/logical"
"github.com/hashicorp/vault/logical/framework" "github.com/hashicorp/vault/logical/framework"
) )
const SecretCredsType = "creds" const SecretCredsType = "creds"
// Default Revoke and Drop user SQL statment
// Revoking permissions for the user is done before the
// drop, because MySQL explicitly documents that open user connections
// will not be closed. By revoking all grants, at least we ensure
// that the open connection is useless.
// Dropping the user will only affect the next connection
// This is not a prepared statement because not all commands are supported
// 1295: This command is not supported in the prepared statement protocol yet
// Reference https://mariadb.com/kb/en/mariadb/prepare-statement/
const defaultRevokeSQL = `
REVOKE ALL PRIVILEGES, GRANT OPTION FROM '{{name}}'@'%';
DROP USER '{{name}}'@'%'
`
func secretCreds(b *backend) *framework.Secret { func secretCreds(b *backend) *framework.Secret {
return &framework.Secret{ return &framework.Secret{
Type: SecretCredsType, Type: SecretCredsType,
@ -22,6 +38,11 @@ func secretCreds(b *backend) *framework.Secret {
Type: framework.TypeString, Type: framework.TypeString,
Description: "Password", Description: "Password",
}, },
"role": &framework.FieldSchema{
Type: framework.TypeString,
Description: "Role",
},
}, },
Renew: b.secretCredsRenew, Renew: b.secretCredsRenew,
@ -46,6 +67,7 @@ func (b *backend) secretCredsRenew(
func (b *backend) secretCredsRevoke( func (b *backend) secretCredsRevoke(
req *logical.Request, d *framework.FieldData) (*logical.Response, error) { req *logical.Request, d *framework.FieldData) (*logical.Response, error) {
// Get the username from the internal data // Get the username from the internal data
usernameRaw, ok := req.Secret.InternalData["username"] usernameRaw, ok := req.Secret.InternalData["username"]
if !ok { if !ok {
@ -59,6 +81,44 @@ func (b *backend) secretCredsRevoke(
return nil, err return nil, err
} }
// Get the role name
// we may not always have role data in the secret InternalData
// so don't exit if the roleNameRaw fails. Instead it is set
// as an empty string.
var roleName string
roleNameRaw, ok := req.Secret.InternalData["role"]
if !ok {
roleName = ""
} else {
roleName, _ = roleNameRaw.(string)
}
// init default revoke sql string.
// this will replaced by a user provided one if one exists
// otherwise this is what will be used when lease is revoked
revokeSQL := defaultRevokeSQL
// init bool to track if we should responding with warning about nil role
nonNilResponse := false
// if we were successful in finding a role name
// create role entry from that name
if roleName != "" {
role, err := b.Role(req.Storage, roleName)
if err != nil {
return nil, err
}
if role == nil {
nonNilResponse = true
}
// Check for a revokeSQL string
// if one exists use that instead of the default
if role.RevokeSQL != "" && role != nil {
revokeSQL = role.RevokeSQL
}
}
// Start a transaction // Start a transaction
tx, err := db.Begin() tx, err := db.Begin()
if err != nil { if err != nil {
@ -66,25 +126,38 @@ func (b *backend) secretCredsRevoke(
} }
defer tx.Rollback() defer tx.Rollback()
// Revoke all permissions for the user. This is done before the for _, query := range strutil.ParseArbitraryStringSlice(revokeSQL, ";") {
// drop, because MySQL explicitly documents that open user connections query = strings.TrimSpace(query)
// will not be closed. By revoking all grants, at least we ensure if len(query) == 0 {
// that the open connection is useless. continue
_, err = tx.Exec("REVOKE ALL PRIVILEGES, GRANT OPTION FROM '" + username + "'@'%'") }
if err != nil {
return nil, err // This is not a prepared statement because not all commands are supported
} // 1295: This command is not supported in the prepared statement protocol yet
// Reference https://mariadb.com/kb/en/mariadb/prepare-statement/
query = strings.Replace(query, "{{name}}", username, -1)
_, err = tx.Exec(query)
if err != nil {
return nil, err
}
// Drop this user. This only affects the next connection, which is
// why we do the revoke initially.
_, err = tx.Exec("DROP USER '" + username + "'@'%'")
if err != nil {
return nil, err
} }
// Commit the transaction // Commit the transaction
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
return nil, err return nil, err
} }
// Let the user know that since we had a nil role we used the default SQL revocation statment
if nonNilResponse == true {
// unable to get role continuing with default sql statements for revoking users
var resp *logical.Response
resp = &logical.Response{}
resp.AddWarning("Role " + roleName + "cannot be found. Using default SQL for revoking user")
// return non-nil response and nil error
return resp, nil
}
return nil, nil return nil, nil
} }