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 (
"context"
"errors"
"fmt"
"strings"
"time"
@@ -88,32 +87,6 @@ func fieldsForType(roleType string) map[string]*framework.FieldSchema {
Type: framework.TypeString,
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
@@ -141,11 +114,36 @@ func dynamicFields() map[string]*framework.FieldSchema {
Type: framework.TypeDurationSecond,
Description: "Default ttl for role.",
},
"max_ttl": {
Type: framework.TypeDurationSecond,
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
}
@@ -172,13 +170,6 @@ func staticFields() map[string]*framework.FieldSchema {
this functionality. See the plugin's API page for more information on
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
}
@@ -219,34 +210,7 @@ func (b *databaseBackend) pathStaticRoleDelete(ctx context.Context, req *logical
// Remove the item from the queue
_, _ = b.popFromRotationQueueByKey(name)
// If this role is a static account, we need to revoke the user from the
// 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)
err := req.Storage.Delete(ctx, databaseStaticRolePath+name)
if err != nil {
return nil, err
}
@@ -262,14 +226,24 @@ func (b *databaseBackend) pathStaticRoleRead(ctx context.Context, req *logical.R
if role == 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 {
data["username"] = role.StaticAccount.Username
data["rotation_statements"] = role.Statements.Rotation
data["rotation_period"] = role.StaticAccount.RotationPeriod.Seconds()
if !role.StaticAccount.LastVaultRotation.IsZero() {
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{
@@ -286,19 +260,12 @@ func (b *databaseBackend) pathRoleRead(ctx context.Context, req *logical.Request
return nil, nil
}
return &logical.Response{
Data: pathRoleReadCommon(role),
}, nil
}
func pathRoleReadCommon(role *roleEntry) map[string]interface{} {
data := map[string]interface{}{
"db_name": role.DBName,
"creation_statements": role.Statements.Creation,
"revocation_statements": role.Statements.Revocation,
"rollback_statements": role.Statements.Rollback,
"renew_statements": role.Statements.Renewal,
"rotation_statements": role.Statements.Rotation,
"default_ttl": role.DefaultTTL.Seconds(),
"max_ttl": role.MaxTTL.Seconds(),
}
@@ -314,10 +281,10 @@ func pathRoleReadCommon(role *roleEntry) map[string]interface{} {
if len(role.Statements.Renewal) == 0 {
data["renew_statements"] = []string{}
}
if len(role.Statements.Rotation) == 0 {
data["rotation_statements"] = []string{}
}
return data
return &logical.Response{
Data: data,
}, nil
}
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{}
}
if err := pathRoleCreateUpdateCommon(ctx, role, req.Operation, data); err != nil {
return logical.ErrorResponse(err.Error()), nil
createOperation := (req.Operation == logical.CreateOperation)
// 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
{
if defaultTTLRaw, ok := data.GetOk("default_ttl"); ok {
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
}
if maxTTLRaw, ok := data.GetOk("max_ttl"); ok {
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
}
}
@@ -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
// can be used later by database plugins that distinguish between creating and
// updating roles, and may use seperate statements depending on the context.
createRole := req.Operation == logical.CreateOperation
createRole := (req.Operation == logical.CreateOperation)
if role == nil {
role = &roleEntry{
StaticAccount: &staticAccount{},
@@ -422,8 +434,15 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l
createRole = true
}
if err := pathRoleCreateUpdateCommon(ctx, role, req.Operation, data); err != nil {
return logical.ErrorResponse(err.Error()), nil
// DB Attributes
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)
@@ -458,8 +477,6 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l
role.Statements.Rotation = data.Get("rotation_statements").([]string)
}
role.StaticAccount.RevokeUserOnDelete = data.Get("revoke_user_on_delete").(bool)
// lvr represents the roles' LastVaultRotation
lvr := role.StaticAccount.LastVaultRotation
@@ -504,57 +521,6 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l
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 {
DBName string `json:"db_name"`
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)
defer cleanup()
// create the database user
createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate)
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
@@ -62,17 +65,17 @@ func TestBackend_StaticRole_Config(t *testing.T) {
}{
"basic": {
account: map[string]interface{}{
"username": "statictest",
"username": dbUser,
"rotation_period": "5400s",
},
expected: map[string]interface{}{
"username": "statictest",
"username": dbUser,
"rotation_period": float64(5400),
},
},
"missing rotation period": {
account: map[string]interface{}{
"username": "statictest",
"username": dbUser,
},
err: errors.New("rotation_period is required to create static accounts"),
},
@@ -83,11 +86,7 @@ func TestBackend_StaticRole_Config(t *testing.T) {
data := map[string]interface{}{
"name": "plugin-role-test",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"default_ttl": "5m",
"max_ttl": "10m",
}
for k, v := range tc.account {
@@ -205,6 +204,9 @@ func TestBackend_StaticRole_Updates(t *testing.T) {
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup()
// create the database user
createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate)
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
@@ -228,12 +230,8 @@ func TestBackend_StaticRole_Updates(t *testing.T) {
data = map[string]interface{}{
"name": "plugin-role-test-updates",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"default_ttl": "5m",
"max_ttl": "10m",
"username": "statictest",
"username": dbUser,
"rotation_period": "5400s",
}
@@ -285,7 +283,7 @@ func TestBackend_StaticRole_Updates(t *testing.T) {
updateData := map[string]interface{}{
"name": "plugin-role-test-updates",
"db_name": "plugin-test",
"username": "statictest",
"username": dbUser,
"rotation_period": "6400s",
}
req = &logical.Request{
@@ -340,7 +338,7 @@ func TestBackend_StaticRole_Updates(t *testing.T) {
updateData = map[string]interface{}{
"name": "plugin-role-test-updates",
"db_name": "plugin-test",
"username": "statictest",
"username": dbUser,
"rotation_statements": testRoleStaticUpdateRotation,
}
req = &logical.Request{
@@ -399,6 +397,9 @@ func TestBackend_StaticRole_Role_name_check(t *testing.T) {
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup()
// create the database user
createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate)
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
@@ -468,10 +469,8 @@ func TestBackend_StaticRole_Role_name_check(t *testing.T) {
data = map[string]interface{}{
"name": "plugin-role-test-2",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"username": "testusername",
"username": dbUser,
"rotation_period": "1h",
}
@@ -514,7 +513,6 @@ const testRoleStaticCreate = `
CREATE ROLE "{{name}}" WITH
LOGIN
PASSWORD '{{password}}';
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";
`
const testRoleStaticUpdate = `

View File

@@ -2,6 +2,7 @@ package database
import (
"context"
"log"
"strings"
"testing"
"time"
@@ -10,11 +11,14 @@ import (
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/framework"
"github.com/hashicorp/vault/sdk/helper/dbtxn"
"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) {
cluster, sys := getCluster(t)
defer cluster.Cleanup()
@@ -36,6 +40,11 @@ func TestBackend_StaticRole_Rotate_basic(t *testing.T) {
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup()
// create the database user
createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate)
verifyPgConn(t, dbUser, "password", connURL)
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
@@ -59,10 +68,8 @@ func TestBackend_StaticRole_Rotate_basic(t *testing.T) {
data = map[string]interface{}{
"name": "plugin-role-test",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"username": "statictest",
"username": dbUser,
"rotation_period": "5400s",
}
@@ -99,9 +106,7 @@ func TestBackend_StaticRole_Rotate_basic(t *testing.T) {
}
// Verify username/password
if err := verifyPgConn(t, username, password, connURL); err != nil {
t.Fatal(err)
}
verifyPgConn(t, dbUser, password, connURL)
// Re-read the creds, verifying they aren't changing on read
data = map[string]interface{}{}
@@ -156,9 +161,7 @@ func TestBackend_StaticRole_Rotate_basic(t *testing.T) {
}
// Verify new username/password
if err := verifyPgConn(t, username, newPassword, connURL); err != nil {
t.Fatal(err)
}
verifyPgConn(t, username, newPassword, connURL)
}
// 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)
defer cleanup()
// create the database user
createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate)
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
@@ -245,10 +251,7 @@ func TestBackend_StaticRole_Rotate_NonStaticError(t *testing.T) {
}
// Verify username/password
if err := verifyPgConn(t, username, password, connURL); err != nil {
t.Fatal(err)
}
verifyPgConn(t, dbUser, "password", connURL)
// Trigger rotation
data = map[string]interface{}{"name": "plugin-role-test"}
req = &logical.Request{
@@ -289,6 +292,9 @@ func TestBackend_StaticRole_Revoke_user(t *testing.T) {
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
defer cleanup()
// create the database user
createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate)
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
@@ -333,10 +339,8 @@ func TestBackend_StaticRole_Revoke_user(t *testing.T) {
data = map[string]interface{}{
"name": "plugin-role-test",
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"username": "statictest",
"username": dbUser,
"rotation_period": "5400s",
}
if tc.revoke != nil {
@@ -376,9 +380,7 @@ func TestBackend_StaticRole_Revoke_user(t *testing.T) {
}
// Verify username/password
if err := verifyPgConn(t, username, password, connURL); err != nil {
t.Fatal(err)
}
verifyPgConn(t, username, password, connURL)
// delete the role, expect the default where the user is not destroyed
// Read the creds
@@ -394,25 +396,58 @@ func TestBackend_StaticRole_Revoke_user(t *testing.T) {
}
// Verify new username/password still work
if err := verifyPgConn(t, username, password, connURL); err != nil {
if !tc.expectVerifyErr {
t.Fatal(err)
}
}
verifyPgConn(t, username, password, connURL)
})
}
}
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)
db, err := sql.Open("postgres", cURL)
if err != nil {
return err
t.Fatal(err)
}
if err := db.Ping(); err != nil {
return err
t.Fatal(err)
}
return db.Close()
}
// 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)
defer cleanup()
// create the database user
createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate)
// Configure a connection
data := map[string]interface{}{
"connection_url": connURL,
@@ -510,10 +548,8 @@ func TestBackend_Static_QueueWAL_discard_role_newer_rotation_date(t *testing.T)
data = map[string]interface{}{
"name": roleName,
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"username": "statictest",
"username": dbUser,
// Low value here, to make sure the backend rotates this password at least
// once before we compare it to the WAL
"rotation_period": "10s",
@@ -548,7 +584,7 @@ func TestBackend_Static_QueueWAL_discard_role_newer_rotation_date(t *testing.T)
RoleName: roleName,
NewPassword: walPassword,
LastVaultRotation: oldRotationTime,
Username: "statictest",
Username: dbUser,
})
if err != nil {
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
cleanup, connURL := preparePostgresTestContainer(t, config.StorageView, b)
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
data := map[string]interface{}{
@@ -685,16 +726,13 @@ func TestBackend_StaticRole_Rotations_PostgreSQL(t *testing.T) {
}
// Create three static roles with different rotation periods
testCases := []string{"65", "130", "5400"}
for _, tc := range testCases {
roleName := "plugin-static-role-" + tc
data = map[string]interface{}{
"name": roleName,
"db_name": "plugin-test",
"creation_statements": testRoleStaticCreate,
"rotation_statements": testRoleStaticUpdate,
"revocation_statements": defaultRevocationSQL,
"username": "statictest" + tc,
"username": dbUser + tc,
"rotation_period": tc,
}

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

View File

@@ -12,6 +12,8 @@ import (
"github.com/hashicorp/vault/helper/testhelpers/docker"
"github.com/hashicorp/vault/sdk/database/dbplugin"
"github.com/hashicorp/vault/sdk/helper/dbtxn"
"github.com/lib/pq"
"github.com/ory/dockertest"
)
@@ -321,6 +323,10 @@ func TestPostgresSQL_SetCredentials(t *testing.T) {
cleanup, connURL := preparePostgresTestContainer(t)
defer cleanup()
// create the database user
dbUser := "vaultstatictest"
createTestPGUser(t, connURL, dbUser, "password", testRoleStaticCreate)
connectionDetails := map[string]interface{}{
"connection_url": connURL,
}
@@ -337,18 +343,18 @@ func TestPostgresSQL_SetCredentials(t *testing.T) {
}
usernameConfig := dbplugin.StaticUserConfig{
Username: "test",
Username: dbUser,
Password: password,
}
// Test with no configured Creation Statement
// Test with no configured Rotation Statement
username, password, err := db.SetCredentials(context.Background(), dbplugin.Statements{}, usernameConfig)
if err == nil {
t.Fatalf("err: %s", err)
}
statements := dbplugin.Statements{
Creation: []string{testPostgresStaticRole},
Rotation: []string{testPostgresStaticRoleRotate},
}
// User should not exist, make sure we can create
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)
}
// call SetCredentials again, the user will already exist, password will
// change. Without rotation statements, this should use the defaults
// call SetCredentials again, password will change
newPassword, _ := db.GenerateCredentials(context.Background())
usernameConfig.Password = newPassword
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 {
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 {
@@ -484,6 +472,12 @@ CREATE ROLE "{{name}}" WITH
GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO "{{name}}";
`
const testRoleStaticCreate = `
CREATE ROLE "{{name}}" WITH
LOGIN
PASSWORD '{{password}}';
`
const testPostgresStaticRoleRotate = `
ALTER ROLE "{{name}}" WITH PASSWORD '{{password}}';
`
@@ -491,3 +485,42 @@ ALTER ROLE "{{name}}" WITH PASSWORD '{{password}}';
const testPostgresStaticRoleGrant = `
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
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
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
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
@@ -462,7 +438,6 @@ Static Roles, please see the database-specific documentation.
{
"db_name": "mysql",
"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_period": "1h"
}
@@ -506,13 +481,8 @@ $ curl \
"data": {
"db_name": "mysql",
"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_period":"1h",
"renew_statements": [],
"revocation_statements": [],
"rollback_statements": []
"revoke_user_on_delete": false,
},
}
```