Combined Database backend: remove create/delete support (#6951)

* remove create/update database user for static accounts

* update tests after create/delete removed

* small cleanups

* update postgresql setcredentials test
This commit is contained in:
Clint
2019-06-23 15:58:07 -05:00
committed by GitHub
parent d8e3c35af5
commit dc4e378f75
6 changed files with 279 additions and 281 deletions

View File

@@ -2,7 +2,6 @@ package database
import ( import (
"context" "context"
"errors"
"fmt" "fmt"
"strings" "strings"
"time" "time"
@@ -88,32 +87,6 @@ func fieldsForType(roleType string) map[string]*framework.FieldSchema {
Type: framework.TypeString, Type: framework.TypeString,
Description: "Name of the database this role acts on.", Description: "Name of the database this role acts on.",
}, },
"creation_statements": {
Type: framework.TypeStringSlice,
Description: `Specifies the database statements executed to
create and configure a user. See the plugin's API page for more
information on support and formatting for this parameter.`,
},
"revocation_statements": {
Type: framework.TypeStringSlice,
Description: `Specifies the database statements to be executed
to revoke a user. See the plugin's API page for more information
on support and formatting for this parameter.`,
},
"renew_statements": {
Type: framework.TypeStringSlice,
Description: `Specifies the database statements to be executed
to renew a user. Not every plugin type will support this
functionality. See the plugin's API page for more information on
support and formatting for this parameter. `,
},
"rollback_statements": {
Type: framework.TypeStringSlice,
Description: `Specifies the database statements to be executed
rollback a create operation in the event of an error. Not every plugin
type will support this functionality. See the plugin's API page for
more information on support and formatting for this parameter.`,
},
} }
// Get the fields that are specific to the type of role, and add them to the // Get the fields that are specific to the type of role, and add them to the
@@ -141,11 +114,36 @@ func dynamicFields() map[string]*framework.FieldSchema {
Type: framework.TypeDurationSecond, Type: framework.TypeDurationSecond,
Description: "Default ttl for role.", Description: "Default ttl for role.",
}, },
"max_ttl": { "max_ttl": {
Type: framework.TypeDurationSecond, Type: framework.TypeDurationSecond,
Description: "Maximum time a credential is valid for", Description: "Maximum time a credential is valid for",
}, },
"creation_statements": {
Type: framework.TypeStringSlice,
Description: `Specifies the database statements executed to
create and configure a user. See the plugin's API page for more
information on support and formatting for this parameter.`,
},
"revocation_statements": {
Type: framework.TypeStringSlice,
Description: `Specifies the database statements to be executed
to revoke a user. See the plugin's API page for more information
on support and formatting for this parameter.`,
},
"renew_statements": {
Type: framework.TypeStringSlice,
Description: `Specifies the database statements to be executed
to renew a user. Not every plugin type will support this
functionality. See the plugin's API page for more information on
support and formatting for this parameter. `,
},
"rollback_statements": {
Type: framework.TypeStringSlice,
Description: `Specifies the database statements to be executed
rollback a create operation in the event of an error. Not every plugin
type will support this functionality. See the plugin's API page for
more information on support and formatting for this parameter.`,
},
} }
return fields return fields
} }
@@ -172,13 +170,6 @@ func staticFields() map[string]*framework.FieldSchema {
this functionality. See the plugin's API page for more information on this functionality. See the plugin's API page for more information on
support and formatting for this parameter.`, support and formatting for this parameter.`,
}, },
"revoke_user_on_delete": {
Type: framework.TypeBool,
Default: false,
Description: `Revoke the database user identified by the username when
this Role is deleted. Revocation will use the configured
revocation_statements if provided. Default false.`,
},
} }
return fields return fields
} }
@@ -219,34 +210,7 @@ func (b *databaseBackend) pathStaticRoleDelete(ctx context.Context, req *logical
// Remove the item from the queue // Remove the item from the queue
_, _ = b.popFromRotationQueueByKey(name) _, _ = b.popFromRotationQueueByKey(name)
// If this role is a static account, we need to revoke the user from the err := req.Storage.Delete(ctx, databaseStaticRolePath+name)
// database
role, err := b.StaticRole(ctx, req.Storage, name)
if err != nil {
return nil, err
}
if role == nil {
return nil, nil
}
// Clean up the static useraccount, if it exists
revoke := role.StaticAccount.RevokeUserOnDelete
if revoke {
db, err := b.GetConnection(ctx, req.Storage, role.DBName)
if err != nil {
return nil, err
}
db.RLock()
defer db.RUnlock()
if err := db.RevokeUser(ctx, role.Statements, role.StaticAccount.Username); err != nil {
b.CloseIfShutdown(db, err)
return nil, err
}
}
err = req.Storage.Delete(ctx, databaseStaticRolePath+name)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -262,14 +226,24 @@ func (b *databaseBackend) pathStaticRoleRead(ctx context.Context, req *logical.R
if role == nil { if role == nil {
return nil, nil return nil, nil
} }
data := pathRoleReadCommon(role)
data := map[string]interface{}{
"db_name": role.DBName,
"rotation_statements": role.Statements.Rotation,
}
// guard against nil StaticAccount; shouldn't happen but we'll be safe
if role.StaticAccount != nil { if role.StaticAccount != nil {
data["username"] = role.StaticAccount.Username data["username"] = role.StaticAccount.Username
data["rotation_statements"] = role.Statements.Rotation
data["rotation_period"] = role.StaticAccount.RotationPeriod.Seconds() data["rotation_period"] = role.StaticAccount.RotationPeriod.Seconds()
if !role.StaticAccount.LastVaultRotation.IsZero() { if !role.StaticAccount.LastVaultRotation.IsZero() {
data["last_vault_rotation"] = role.StaticAccount.LastVaultRotation data["last_vault_rotation"] = role.StaticAccount.LastVaultRotation
} }
data["revoke_user_on_delete"] = role.StaticAccount.RevokeUserOnDelete }
if len(role.Statements.Rotation) == 0 {
data["rotation_statements"] = []string{}
} }
return &logical.Response{ return &logical.Response{
@@ -286,19 +260,12 @@ func (b *databaseBackend) pathRoleRead(ctx context.Context, req *logical.Request
return nil, nil return nil, nil
} }
return &logical.Response{
Data: pathRoleReadCommon(role),
}, nil
}
func pathRoleReadCommon(role *roleEntry) map[string]interface{} {
data := map[string]interface{}{ data := map[string]interface{}{
"db_name": role.DBName, "db_name": role.DBName,
"creation_statements": role.Statements.Creation, "creation_statements": role.Statements.Creation,
"revocation_statements": role.Statements.Revocation, "revocation_statements": role.Statements.Revocation,
"rollback_statements": role.Statements.Rollback, "rollback_statements": role.Statements.Rollback,
"renew_statements": role.Statements.Renewal, "renew_statements": role.Statements.Renewal,
"rotation_statements": role.Statements.Rotation,
"default_ttl": role.DefaultTTL.Seconds(), "default_ttl": role.DefaultTTL.Seconds(),
"max_ttl": role.MaxTTL.Seconds(), "max_ttl": role.MaxTTL.Seconds(),
} }
@@ -314,10 +281,10 @@ func pathRoleReadCommon(role *roleEntry) map[string]interface{} {
if len(role.Statements.Renewal) == 0 { if len(role.Statements.Renewal) == 0 {
data["renew_statements"] = []string{} data["renew_statements"] = []string{}
} }
if len(role.Statements.Rotation) == 0 {
data["rotation_statements"] = []string{} return &logical.Response{
} Data: data,
return data }, nil
} }
func (b *databaseBackend) pathRoleList(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { func (b *databaseBackend) pathRoleList(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
@@ -355,20 +322,65 @@ func (b *databaseBackend) pathRoleCreateUpdate(ctx context.Context, req *logical
role = &roleEntry{} role = &roleEntry{}
} }
if err := pathRoleCreateUpdateCommon(ctx, role, req.Operation, data); err != nil { createOperation := (req.Operation == logical.CreateOperation)
return logical.ErrorResponse(err.Error()), nil
// DB Attributes
{
if dbNameRaw, ok := data.GetOk("db_name"); ok {
role.DBName = dbNameRaw.(string)
} else if createOperation {
role.DBName = data.Get("db_name").(string)
}
if role.DBName == "" {
return logical.ErrorResponse("database name is required"), nil
}
} }
// Statements
{
if creationStmtsRaw, ok := data.GetOk("creation_statements"); ok {
role.Statements.Creation = creationStmtsRaw.([]string)
} else if createOperation {
role.Statements.Creation = data.Get("creation_statements").([]string)
}
if revocationStmtsRaw, ok := data.GetOk("revocation_statements"); ok {
role.Statements.Revocation = revocationStmtsRaw.([]string)
} else if createOperation {
role.Statements.Revocation = data.Get("revocation_statements").([]string)
}
if rollbackStmtsRaw, ok := data.GetOk("rollback_statements"); ok {
role.Statements.Rollback = rollbackStmtsRaw.([]string)
} else if createOperation {
role.Statements.Rollback = data.Get("rollback_statements").([]string)
}
if renewStmtsRaw, ok := data.GetOk("renew_statements"); ok {
role.Statements.Renewal = renewStmtsRaw.([]string)
} else if createOperation {
role.Statements.Renewal = data.Get("renew_statements").([]string)
}
// Do not persist deprecated statements that are populated on role read
role.Statements.CreationStatements = ""
role.Statements.RevocationStatements = ""
role.Statements.RenewStatements = ""
role.Statements.RollbackStatements = ""
}
role.Statements.Revocation = strutil.RemoveEmpty(role.Statements.Revocation)
// TTLs // TTLs
{ {
if defaultTTLRaw, ok := data.GetOk("default_ttl"); ok { if defaultTTLRaw, ok := data.GetOk("default_ttl"); ok {
role.DefaultTTL = time.Duration(defaultTTLRaw.(int)) * time.Second role.DefaultTTL = time.Duration(defaultTTLRaw.(int)) * time.Second
} else if req.Operation == logical.CreateOperation { } else if createOperation {
role.DefaultTTL = time.Duration(data.Get("default_ttl").(int)) * time.Second role.DefaultTTL = time.Duration(data.Get("default_ttl").(int)) * time.Second
} }
if maxTTLRaw, ok := data.GetOk("max_ttl"); ok { if maxTTLRaw, ok := data.GetOk("max_ttl"); ok {
role.MaxTTL = time.Duration(maxTTLRaw.(int)) * time.Second role.MaxTTL = time.Duration(maxTTLRaw.(int)) * time.Second
} else if req.Operation == logical.CreateOperation { } else if createOperation {
role.MaxTTL = time.Duration(data.Get("max_ttl").(int)) * time.Second role.MaxTTL = time.Duration(data.Get("max_ttl").(int)) * time.Second
} }
} }
@@ -414,7 +426,7 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l
// createRole is a boolean to indicate if this is a new role creation. This is // createRole is a boolean to indicate if this is a new role creation. This is
// can be used later by database plugins that distinguish between creating and // can be used later by database plugins that distinguish between creating and
// updating roles, and may use seperate statements depending on the context. // updating roles, and may use seperate statements depending on the context.
createRole := req.Operation == logical.CreateOperation createRole := (req.Operation == logical.CreateOperation)
if role == nil { if role == nil {
role = &roleEntry{ role = &roleEntry{
StaticAccount: &staticAccount{}, StaticAccount: &staticAccount{},
@@ -422,8 +434,15 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l
createRole = true createRole = true
} }
if err := pathRoleCreateUpdateCommon(ctx, role, req.Operation, data); err != nil { // DB Attributes
return logical.ErrorResponse(err.Error()), nil if dbNameRaw, ok := data.GetOk("db_name"); ok {
role.DBName = dbNameRaw.(string)
} else if createRole {
role.DBName = data.Get("db_name").(string)
}
if role.DBName == "" {
return logical.ErrorResponse("database name is a required field"), nil
} }
username := data.Get("username").(string) username := data.Get("username").(string)
@@ -458,8 +477,6 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l
role.Statements.Rotation = data.Get("rotation_statements").([]string) role.Statements.Rotation = data.Get("rotation_statements").([]string)
} }
role.StaticAccount.RevokeUserOnDelete = data.Get("revoke_user_on_delete").(bool)
// lvr represents the roles' LastVaultRotation // lvr represents the roles' LastVaultRotation
lvr := role.StaticAccount.LastVaultRotation lvr := role.StaticAccount.LastVaultRotation
@@ -504,57 +521,6 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l
return nil, nil return nil, nil
} }
func pathRoleCreateUpdateCommon(ctx context.Context, role *roleEntry, operation logical.Operation, data *framework.FieldData) error {
// DB Attributes
{
if dbNameRaw, ok := data.GetOk("db_name"); ok {
role.DBName = dbNameRaw.(string)
} else if operation == logical.CreateOperation {
role.DBName = data.Get("db_name").(string)
}
if role.DBName == "" {
return errors.New("empty database name attribute")
}
}
// Statements
{
if creationStmtsRaw, ok := data.GetOk("creation_statements"); ok {
role.Statements.Creation = creationStmtsRaw.([]string)
} else if operation == logical.CreateOperation {
role.Statements.Creation = data.Get("creation_statements").([]string)
}
if revocationStmtsRaw, ok := data.GetOk("revocation_statements"); ok {
role.Statements.Revocation = revocationStmtsRaw.([]string)
} else if operation == logical.CreateOperation {
role.Statements.Revocation = data.Get("revocation_statements").([]string)
}
if rollbackStmtsRaw, ok := data.GetOk("rollback_statements"); ok {
role.Statements.Rollback = rollbackStmtsRaw.([]string)
} else if operation == logical.CreateOperation {
role.Statements.Rollback = data.Get("rollback_statements").([]string)
}
if renewStmtsRaw, ok := data.GetOk("renew_statements"); ok {
role.Statements.Renewal = renewStmtsRaw.([]string)
} else if operation == logical.CreateOperation {
role.Statements.Renewal = data.Get("renew_statements").([]string)
}
// Do not persist deprecated statements that are populated on role read
role.Statements.CreationStatements = ""
role.Statements.RevocationStatements = ""
role.Statements.RenewStatements = ""
role.Statements.RollbackStatements = ""
}
role.Statements.Revocation = strutil.RemoveEmpty(role.Statements.Revocation)
return nil
}
type roleEntry struct { type roleEntry struct {
DBName string `json:"db_name"` DBName string `json:"db_name"`
Statements dbplugin.Statements `json:"statements"` Statements dbplugin.Statements `json:"statements"`

View File

@@ -34,6 +34,9 @@ func TestBackend_StaticRole_Config(t *testing.T) {
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b) cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup() defer cleanup()
// create the database user
createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate)
// Configure a connection // Configure a connection
data := map[string]interface{}{ data := map[string]interface{}{
"connection_url": connURL, "connection_url": connURL,
@@ -62,17 +65,17 @@ func TestBackend_StaticRole_Config(t *testing.T) {
}{ }{
"basic": { "basic": {
account: map[string]interface{}{ account: map[string]interface{}{
"username": "statictest", "username": dbUser,
"rotation_period": "5400s", "rotation_period": "5400s",
}, },
expected: map[string]interface{}{ expected: map[string]interface{}{
"username": "statictest", "username": dbUser,
"rotation_period": float64(5400), "rotation_period": float64(5400),
}, },
}, },
"missing rotation period": { "missing rotation period": {
account: map[string]interface{}{ account: map[string]interface{}{
"username": "statictest", "username": dbUser,
}, },
err: errors.New("rotation_period is required to create static accounts"), err: errors.New("rotation_period is required to create static accounts"),
}, },
@@ -81,13 +84,9 @@ func TestBackend_StaticRole_Config(t *testing.T) {
for name, tc := range testCases { for name, tc := range testCases {
t.Run(name, func(t *testing.T) { t.Run(name, func(t *testing.T) {
data := map[string]interface{}{ data := map[string]interface{}{
"name": "plugin-role-test", "name": "plugin-role-test",
"db_name": "plugin-test", "db_name": "plugin-test",
"creation_statements": testRoleStaticCreate, "rotation_statements": testRoleStaticUpdate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"default_ttl": "5m",
"max_ttl": "10m",
} }
for k, v := range tc.account { for k, v := range tc.account {
@@ -205,6 +204,9 @@ func TestBackend_StaticRole_Updates(t *testing.T) {
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b) cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup() defer cleanup()
// create the database user
createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate)
// Configure a connection // Configure a connection
data := map[string]interface{}{ data := map[string]interface{}{
"connection_url": connURL, "connection_url": connURL,
@@ -226,15 +228,11 @@ func TestBackend_StaticRole_Updates(t *testing.T) {
} }
data = map[string]interface{}{ data = map[string]interface{}{
"name": "plugin-role-test-updates", "name": "plugin-role-test-updates",
"db_name": "plugin-test", "db_name": "plugin-test",
"creation_statements": testRoleStaticCreate, "rotation_statements": testRoleStaticUpdate,
"rotation_statements": testRoleStaticUpdate, "username": dbUser,
"revocation_statements": defaultRevocationSQL, "rotation_period": "5400s",
"default_ttl": "5m",
"max_ttl": "10m",
"username": "statictest",
"rotation_period": "5400s",
} }
req = &logical.Request{ req = &logical.Request{
@@ -285,7 +283,7 @@ func TestBackend_StaticRole_Updates(t *testing.T) {
updateData := map[string]interface{}{ updateData := map[string]interface{}{
"name": "plugin-role-test-updates", "name": "plugin-role-test-updates",
"db_name": "plugin-test", "db_name": "plugin-test",
"username": "statictest", "username": dbUser,
"rotation_period": "6400s", "rotation_period": "6400s",
} }
req = &logical.Request{ req = &logical.Request{
@@ -340,7 +338,7 @@ func TestBackend_StaticRole_Updates(t *testing.T) {
updateData = map[string]interface{}{ updateData = map[string]interface{}{
"name": "plugin-role-test-updates", "name": "plugin-role-test-updates",
"db_name": "plugin-test", "db_name": "plugin-test",
"username": "statictest", "username": dbUser,
"rotation_statements": testRoleStaticUpdateRotation, "rotation_statements": testRoleStaticUpdateRotation,
} }
req = &logical.Request{ req = &logical.Request{
@@ -399,6 +397,9 @@ func TestBackend_StaticRole_Role_name_check(t *testing.T) {
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b) cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup() defer cleanup()
// create the database user
createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate)
// Configure a connection // Configure a connection
data := map[string]interface{}{ data := map[string]interface{}{
"connection_url": connURL, "connection_url": connURL,
@@ -466,13 +467,11 @@ func TestBackend_StaticRole_Role_name_check(t *testing.T) {
// repeat, with a static role first // repeat, with a static role first
data = map[string]interface{}{ data = map[string]interface{}{
"name": "plugin-role-test-2", "name": "plugin-role-test-2",
"db_name": "plugin-test", "db_name": "plugin-test",
"creation_statements": testRoleStaticCreate, "rotation_statements": testRoleStaticUpdate,
"rotation_statements": testRoleStaticUpdate, "username": dbUser,
"revocation_statements": defaultRevocationSQL, "rotation_period": "1h",
"username": "testusername",
"rotation_period": "1h",
} }
req = &logical.Request{ req = &logical.Request{
@@ -514,7 +513,6 @@ const testRoleStaticCreate = `
CREATE ROLE "{{name}}" WITH CREATE ROLE "{{name}}" WITH
LOGIN LOGIN
PASSWORD '{{password}}'; PASSWORD '{{password}}';
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";
` `
const testRoleStaticUpdate = ` const testRoleStaticUpdate = `

View File

@@ -2,6 +2,7 @@ package database
import ( import (
"context" "context"
"log"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -10,11 +11,14 @@ import (
"github.com/hashicorp/vault/helper/namespace" "github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/framework" "github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/dbtxn"
"github.com/hashicorp/vault/sdk/logical" "github.com/hashicorp/vault/sdk/logical"
_ "github.com/lib/pq" "github.com/lib/pq"
) )
const dbUser = "vaultstatictest"
func TestBackend_StaticRole_Rotate_basic(t *testing.T) { func TestBackend_StaticRole_Rotate_basic(t *testing.T) {
cluster, sys := getCluster(t) cluster, sys := getCluster(t)
defer cluster.Cleanup() defer cluster.Cleanup()
@@ -36,6 +40,11 @@ func TestBackend_StaticRole_Rotate_basic(t *testing.T) {
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b) cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup() defer cleanup()
// create the database user
createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate)
verifyPgConn(t, dbUser, "password", connURL)
// Configure a connection // Configure a connection
data := map[string]interface{}{ data := map[string]interface{}{
"connection_url": connURL, "connection_url": connURL,
@@ -57,13 +66,11 @@ func TestBackend_StaticRole_Rotate_basic(t *testing.T) {
} }
data = map[string]interface{}{ data = map[string]interface{}{
"name": "plugin-role-test", "name": "plugin-role-test",
"db_name": "plugin-test", "db_name": "plugin-test",
"creation_statements": testRoleStaticCreate, "rotation_statements": testRoleStaticUpdate,
"rotation_statements": testRoleStaticUpdate, "username": dbUser,
"revocation_statements": defaultRevocationSQL, "rotation_period": "5400s",
"username": "statictest",
"rotation_period": "5400s",
} }
req = &logical.Request{ req = &logical.Request{
@@ -99,9 +106,7 @@ func TestBackend_StaticRole_Rotate_basic(t *testing.T) {
} }
// Verify username/password // Verify username/password
if err := verifyPgConn(t, username, password, connURL); err != nil { verifyPgConn(t, dbUser, password, connURL)
t.Fatal(err)
}
// Re-read the creds, verifying they aren't changing on read // Re-read the creds, verifying they aren't changing on read
data = map[string]interface{}{} data = map[string]interface{}{}
@@ -156,9 +161,7 @@ func TestBackend_StaticRole_Rotate_basic(t *testing.T) {
} }
// Verify new username/password // Verify new username/password
if err := verifyPgConn(t, username, newPassword, connURL); err != nil { verifyPgConn(t, username, newPassword, connURL)
t.Fatal(err)
}
} }
// Sanity check to make sure we don't allow an attempt of rotating credentials // Sanity check to make sure we don't allow an attempt of rotating credentials
@@ -185,6 +188,9 @@ func TestBackend_StaticRole_Rotate_NonStaticError(t *testing.T) {
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b) cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup() defer cleanup()
// create the database user
createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate)
// Configure a connection // Configure a connection
data := map[string]interface{}{ data := map[string]interface{}{
"connection_url": connURL, "connection_url": connURL,
@@ -245,10 +251,7 @@ func TestBackend_StaticRole_Rotate_NonStaticError(t *testing.T) {
} }
// Verify username/password // Verify username/password
if err := verifyPgConn(t, username, password, connURL); err != nil { verifyPgConn(t, dbUser, "password", connURL)
t.Fatal(err)
}
// Trigger rotation // Trigger rotation
data = map[string]interface{}{"name": "plugin-role-test"} data = map[string]interface{}{"name": "plugin-role-test"}
req = &logical.Request{ req = &logical.Request{
@@ -289,6 +292,9 @@ func TestBackend_StaticRole_Revoke_user(t *testing.T) {
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b) cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup() defer cleanup()
// create the database user
createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate)
// Configure a connection // Configure a connection
data := map[string]interface{}{ data := map[string]interface{}{
"connection_url": connURL, "connection_url": connURL,
@@ -331,13 +337,11 @@ func TestBackend_StaticRole_Revoke_user(t *testing.T) {
for k, tc := range testCases { for k, tc := range testCases {
t.Run(k, func(t *testing.T) { t.Run(k, func(t *testing.T) {
data = map[string]interface{}{ data = map[string]interface{}{
"name": "plugin-role-test", "name": "plugin-role-test",
"db_name": "plugin-test", "db_name": "plugin-test",
"creation_statements": testRoleStaticCreate, "rotation_statements": testRoleStaticUpdate,
"rotation_statements": testRoleStaticUpdate, "username": dbUser,
"revocation_statements": defaultRevocationSQL, "rotation_period": "5400s",
"username": "statictest",
"rotation_period": "5400s",
} }
if tc.revoke != nil { if tc.revoke != nil {
data["revoke_user_on_delete"] = *tc.revoke data["revoke_user_on_delete"] = *tc.revoke
@@ -376,9 +380,7 @@ func TestBackend_StaticRole_Revoke_user(t *testing.T) {
} }
// Verify username/password // Verify username/password
if err := verifyPgConn(t, username, password, connURL); err != nil { verifyPgConn(t, username, password, connURL)
t.Fatal(err)
}
// delete the role, expect the default where the user is not destroyed // delete the role, expect the default where the user is not destroyed
// Read the creds // Read the creds
@@ -394,25 +396,58 @@ func TestBackend_StaticRole_Revoke_user(t *testing.T) {
} }
// Verify new username/password still work // Verify new username/password still work
if err := verifyPgConn(t, username, password, connURL); err != nil { verifyPgConn(t, username, password, connURL)
if !tc.expectVerifyErr {
t.Fatal(err)
}
}
}) })
} }
} }
func verifyPgConn(t *testing.T, username, password, connURL string) error { func createTestPGUser(t *testing.T, connURL string, username, password, query string) {
t.Helper()
log.Printf("[TRACE] Creating test user")
conn, err := pq.ParseURL(connURL)
if err != nil {
t.Fatal(err)
}
db, err := sql.Open("postgres", conn)
defer db.Close()
if err != nil {
t.Fatal(err)
}
// Start a transaction
ctx := context.Background()
tx, err := db.BeginTx(ctx, nil)
if err != nil {
t.Fatal(err)
}
defer func() {
_ = tx.Rollback()
}()
m := map[string]string{
"name": username,
"password": password,
}
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
t.Fatal(err)
}
// Commit the transaction
if err := tx.Commit(); err != nil {
t.Fatal(err)
}
}
func verifyPgConn(t *testing.T, username, password, connURL string) {
t.Helper()
cURL := strings.Replace(connURL, "postgres:secret", username+":"+password, 1) cURL := strings.Replace(connURL, "postgres:secret", username+":"+password, 1)
db, err := sql.Open("postgres", cURL) db, err := sql.Open("postgres", cURL)
if err != nil { if err != nil {
return err t.Fatal(err)
} }
if err := db.Ping(); err != nil { if err := db.Ping(); err != nil {
return err t.Fatal(err)
} }
return db.Close()
} }
// WAL testing // WAL testing
@@ -482,6 +517,9 @@ func TestBackend_Static_QueueWAL_discard_role_newer_rotation_date(t *testing.T)
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b) cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup() defer cleanup()
// create the database user
createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate)
// Configure a connection // Configure a connection
data := map[string]interface{}{ data := map[string]interface{}{
"connection_url": connURL, "connection_url": connURL,
@@ -508,12 +546,10 @@ func TestBackend_Static_QueueWAL_discard_role_newer_rotation_date(t *testing.T)
// Create role // Create role
data = map[string]interface{}{ data = map[string]interface{}{
"name": roleName, "name": roleName,
"db_name": "plugin-test", "db_name": "plugin-test",
"creation_statements": testRoleStaticCreate, "rotation_statements": testRoleStaticUpdate,
"rotation_statements": testRoleStaticUpdate, "username": dbUser,
"revocation_statements": defaultRevocationSQL,
"username": "statictest",
// Low value here, to make sure the backend rotates this password at least // Low value here, to make sure the backend rotates this password at least
// once before we compare it to the WAL // once before we compare it to the WAL
"rotation_period": "10s", "rotation_period": "10s",
@@ -548,7 +584,7 @@ func TestBackend_Static_QueueWAL_discard_role_newer_rotation_date(t *testing.T)
RoleName: roleName, RoleName: roleName,
NewPassword: walPassword, NewPassword: walPassword,
LastVaultRotation: oldRotationTime, LastVaultRotation: oldRotationTime,
Username: "statictest", Username: dbUser,
}) })
if err != nil { if err != nil {
t.Fatalf("error with PutWAL: %s", err) t.Fatalf("error with PutWAL: %s", err)
@@ -664,6 +700,11 @@ func TestBackend_StaticRole_Rotations_PostgreSQL(t *testing.T) {
// Configure backend, add item and confirm length // Configure backend, add item and confirm length
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b) cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup() defer cleanup()
testCases := []string{"65", "130", "5400"}
// Create database users ahead
for _, tc := range testCases {
createTestPGUser(t, connURL, dbUser+tc, "password", testRoleStaticCreate)
}
// Configure a connection // Configure a connection
data := map[string]interface{}{ data := map[string]interface{}{
@@ -685,17 +726,14 @@ func TestBackend_StaticRole_Rotations_PostgreSQL(t *testing.T) {
} }
// Create three static roles with different rotation periods // Create three static roles with different rotation periods
testCases := []string{"65", "130", "5400"}
for _, tc := range testCases { for _, tc := range testCases {
roleName := "plugin-static-role-" + tc roleName := "plugin-static-role-" + tc
data = map[string]interface{}{ data = map[string]interface{}{
"name": roleName, "name": roleName,
"db_name": "plugin-test", "db_name": "plugin-test",
"creation_statements": testRoleStaticCreate, "rotation_statements": testRoleStaticUpdate,
"rotation_statements": testRoleStaticUpdate, "username": dbUser + tc,
"revocation_statements": defaultRevocationSQL, "rotation_period": tc,
"username": "statictest" + tc,
"rotation_period": tc,
} }
req = &logical.Request{ req = &logical.Request{

View File

@@ -99,8 +99,8 @@ func (p *PostgreSQL) getConnection(ctx context.Context) (*sql.DB, error) {
// passwords in the database in the event an updated database fails to save in // passwords in the database in the event an updated database fails to save in
// Vault's storage. // Vault's storage.
func (p *PostgreSQL) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticUser dbplugin.StaticUserConfig) (username, password string, err error) { func (p *PostgreSQL) SetCredentials(ctx context.Context, statements dbplugin.Statements, staticUser dbplugin.StaticUserConfig) (username, password string, err error) {
if len(statements.Creation) == 0 { if len(statements.Rotation) == 0 {
return "", "", errors.New("empty creation statements") return "", "", errors.New("empty rotation statements")
} }
username = staticUser.Username username = staticUser.Username
@@ -126,16 +126,9 @@ func (p *PostgreSQL) SetCredentials(ctx context.Context, statements dbplugin.Sta
return "", "", err return "", "", err
} }
// Default to using Creation statements, which are required by the Vault // Vault requires the database user already exist, and that the credentials
// backend. If the user exists, use the rotation statements, using the default // used to execute the rotation statements has sufficient privileges.
// ones if there are none provided stmts := statements.Rotation
stmts := statements.Creation
if exists {
stmts = statements.Rotation
if len(stmts) == 0 {
stmts = []string{defaultPostgresRotateCredentialsSQL}
}
}
// Start a transaction // Start a transaction
tx, err := db.BeginTx(ctx, nil) tx, err := db.BeginTx(ctx, nil)

View File

@@ -12,6 +12,8 @@ import (
"github.com/hashicorp/vault/helper/testhelpers/docker" "github.com/hashicorp/vault/helper/testhelpers/docker"
"github.com/hashicorp/vault/sdk/database/dbplugin" "github.com/hashicorp/vault/sdk/database/dbplugin"
"github.com/hashicorp/vault/sdk/helper/dbtxn"
"github.com/lib/pq"
"github.com/ory/dockertest" "github.com/ory/dockertest"
) )
@@ -321,6 +323,10 @@ func TestPostgresSQL_SetCredentials(t *testing.T) {
cleanup, connURL := preparePostgresTestContainer(t) cleanup, connURL := preparePostgresTestContainer(t)
defer cleanup() defer cleanup()
// create the database user
dbUser := "vaultstatictest"
createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate)
connectionDetails := map[string]interface{}{ connectionDetails := map[string]interface{}{
"connection_url": connURL, "connection_url": connURL,
} }
@@ -337,18 +343,18 @@ func TestPostgresSQL_SetCredentials(t *testing.T) {
} }
usernameConfig := dbplugin.StaticUserConfig{ usernameConfig := dbplugin.StaticUserConfig{
Username: "test", Username: dbUser,
Password: password, Password: password,
} }
// Test with no configured Creation Statement // Test with no configured Rotation Statement
username, password, err := db.SetCredentials(context.Background(), dbplugin.Statements{}, usernameConfig) username, password, err := db.SetCredentials(context.Background(), dbplugin.Statements{}, usernameConfig)
if err == nil { if err == nil {
t.Fatalf("err: %s", err) t.Fatalf("err: %s", err)
} }
statements := dbplugin.Statements{ statements := dbplugin.Statements{
Creation: []string{testPostgresStaticRole}, Rotation: []string{testPostgresStaticRoleRotate},
} }
// User should not exist, make sure we can create // User should not exist, make sure we can create
username, password, err = db.SetCredentials(context.Background(), statements, usernameConfig) username, password, err = db.SetCredentials(context.Background(), statements, usernameConfig)
@@ -360,8 +366,7 @@ func TestPostgresSQL_SetCredentials(t *testing.T) {
t.Fatalf("Could not connect with new credentials: %s", err) t.Fatalf("Could not connect with new credentials: %s", err)
} }
// call SetCredentials again, the user will already exist, password will // call SetCredentials again, password will change
// change. Without rotation statements, this should use the defaults
newPassword, _ := db.GenerateCredentials(context.Background()) newPassword, _ := db.GenerateCredentials(context.Background())
usernameConfig.Password = newPassword usernameConfig.Password = newPassword
username, password, err = db.SetCredentials(context.Background(), statements, usernameConfig) username, password, err = db.SetCredentials(context.Background(), statements, usernameConfig)
@@ -376,23 +381,6 @@ func TestPostgresSQL_SetCredentials(t *testing.T) {
if err := testCredsExist(t, connURL, username, password); err != nil { if err := testCredsExist(t, connURL, username, password); err != nil {
t.Fatalf("Could not connect with new credentials: %s", err) t.Fatalf("Could not connect with new credentials: %s", err)
} }
// generate a new password and supply owr own rotation statements
newPassword2, _ := db.GenerateCredentials(context.Background())
usernameConfig.Password = newPassword2
statements.Rotation = []string{testPostgresStaticRoleRotate, testPostgresStaticRoleGrant}
username, password, err = db.SetCredentials(context.Background(), statements, usernameConfig)
if err != nil {
t.Fatalf("err: %s", err)
}
if password != newPassword2 {
t.Fatal("passwords should have changed")
}
if err := testCredsExist(t, connURL, username, password); err != nil {
t.Fatalf("Could not connect with new credentials: %s", err)
}
} }
func testCredsExist(t testing.TB, connURL, username, password string) error { func testCredsExist(t testing.TB, connURL, username, password string) error {
@@ -484,6 +472,12 @@ CREATE ROLE "{{name}}" WITH
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}"; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";
` `
const testRoleStaticCreate = `
CREATE ROLE "{{name}}" WITH
LOGIN
PASSWORD '{{password}}';
`
const testPostgresStaticRoleRotate = ` const testPostgresStaticRoleRotate = `
ALTER ROLE "{{name}}" WITH PASSWORD '{{password}}'; ALTER ROLE "{{name}}" WITH PASSWORD '{{password}}';
` `
@@ -491,3 +485,42 @@ ALTER ROLE "{{name}}" WITH PASSWORD '{{password}}';
const testPostgresStaticRoleGrant = ` const testPostgresStaticRoleGrant = `
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}"; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";
` `
// This is a copy of a test helper method also found in
// builtin/logical/database/rotation_test.go , and should be moved into a shared
// helper file in the future.
func createTestPGUser(t *testing.T, connURL string, username, password, query string) {
t.Helper()
conn, err := pq.ParseURL(connURL)
if err != nil {
t.Fatal(err)
}
db, err := sql.Open("postgres", conn)
defer db.Close()
if err != nil {
t.Fatal(err)
}
// Start a transaction
ctx := context.Background()
tx, err := db.BeginTx(ctx, nil)
if err != nil {
t.Fatal(err)
}
defer func() {
_ = tx.Rollback()
}()
m := map[string]string{
"name": username,
"password": password,
}
if err := dbtxn.ExecuteTxQuery(ctx, tx, m, query); err != nil {
t.Fatal(err)
}
// Commit the transaction
if err := tx.Commit(); err != nil {
t.Fatal(err)
}
}

View File

@@ -425,35 +425,11 @@ Static Roles, please see the database-specific documentation.
- `db_name` `(string: <required>)` - The name of the database connection to use - `db_name` `(string: <required>)` - The name of the database connection to use
for this role. for this role.
- `creation_statements` `(list: <required>)` Specifies the database
statements executed to create and configure a user. See the plugin's API page
for more information on support and formatting for this parameter.
- `revocation_statements` `(list: [])` Specifies the database statements to
be executed to revoke a user. See the plugin's API page for more information
on support and formatting for this parameter.
- `rollback_statements` `(list: [])` Specifies the database statements to be
executed rollback a create operation in the event of an error. Not every
plugin type will support this functionality. See the plugin's API page for
more information on support and formatting for this parameter.
- `renew_statements` `(list: [])` Specifies the database statements to be
executed to renew a user. Not every plugin type will support this
functionality. See the plugin's API page for more information on support and
formatting for this parameter.
- `rotation_statements` `(list: [])` Specifies the database statements to be - `rotation_statements` `(list: [])` Specifies the database statements to be
executed to rotate the password for the configured database user. Not every executed to rotate the password for the configured database user. Not every
plugin type will support this functionality. See the plugin's API page for plugin type will support this functionality. See the plugin's API page for
more information on support and formatting for this parameter. more information on support and formatting for this parameter.
- `revoke_user_on_delete` `(boolean: false)` Specifies if Vault should attempt
to revoke the database user associated with this static role, indicated by the
`username`. If `true`, when Vault deletes this Role it will attempt to revoke
the database user using the configured `revocation_statements` if they exist.
Default `false`
### Sample Payload ### Sample Payload
@@ -462,7 +438,6 @@ Static Roles, please see the database-specific documentation.
{ {
"db_name": "mysql", "db_name": "mysql",
"username": "static-database-user", "username": "static-database-user",
"creation_statements": ["CREATE USER '{{name}}'@'%' IDENTIFIED BY '{{password}}'", "GRANT SELECT ON *.* TO '{{name}}'@'%'"],
"rotation_statements": ["ALTER USER "{{name}}" WITH PASSWORD '{{password}}';"], "rotation_statements": ["ALTER USER "{{name}}" WITH PASSWORD '{{password}}';"],
"rotation_period": "1h" "rotation_period": "1h"
} }
@@ -506,13 +481,8 @@ $ curl \
"data": { "data": {
"db_name": "mysql", "db_name": "mysql",
"username":"static-user", "username":"static-user",
"creation_statements": ["CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}';"], "GRANT SELECT ON ALL TABLES IN SCHEMA public TO \"{{name}}\";"],
"rotation_statements": ["ALTER USER "{{name}}" WITH PASSWORD '{{password}}';"], "rotation_statements": ["ALTER USER "{{name}}" WITH PASSWORD '{{password}}';"],
"rotation_period":"1h", "rotation_period":"1h",
"renew_statements": [],
"revocation_statements": [],
"rollback_statements": []
"revoke_user_on_delete": false,
}, },
} }
``` ```