mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-11-03 03:58:01 +00:00
database: Emit event notifications (#24718)
Including for failures to write credentials and failure to rotate.
This commit is contained in:
committed by
GitHub
parent
47024f060c
commit
55d2dfb3d0
@@ -5,8 +5,10 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/rpc"
|
"net/rpc"
|
||||||
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -398,6 +400,28 @@ func (b *databaseBackend) clean(_ context.Context) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (b *databaseBackend) dbEvent(ctx context.Context,
|
||||||
|
operation string,
|
||||||
|
path string,
|
||||||
|
name string,
|
||||||
|
modified bool,
|
||||||
|
additionalMetadataPairs ...string,
|
||||||
|
) {
|
||||||
|
metadata := []string{
|
||||||
|
logical.EventMetadataModified, strconv.FormatBool(modified),
|
||||||
|
logical.EventMetadataOperation, operation,
|
||||||
|
"path", path,
|
||||||
|
}
|
||||||
|
if name != "" {
|
||||||
|
metadata = append(metadata, "name", name)
|
||||||
|
}
|
||||||
|
metadata = append(metadata, additionalMetadataPairs...)
|
||||||
|
err := logical.SendEvent(ctx, b, fmt.Sprintf("database/%s", operation), metadata...)
|
||||||
|
if err != nil && !errors.Is(err, framework.ErrNoEvents) {
|
||||||
|
b.Logger().Error("Error sending event", "error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const backendHelp = `
|
const backendHelp = `
|
||||||
The database backend supports using many different databases
|
The database backend supports using many different databases
|
||||||
as secret backends, including but not limited to:
|
as secret backends, including but not limited to:
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"slices"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -36,6 +37,7 @@ import (
|
|||||||
"github.com/hashicorp/vault/vault"
|
"github.com/hashicorp/vault/vault"
|
||||||
_ "github.com/jackc/pgx/v4"
|
_ "github.com/jackc/pgx/v4"
|
||||||
"github.com/mitchellh/mapstructure"
|
"github.com/mitchellh/mapstructure"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getClusterPostgresDBWithFactory(t *testing.T, factory logical.Factory) (*vault.TestCluster, logical.SystemView) {
|
func getClusterPostgresDBWithFactory(t *testing.T, factory logical.Factory) (*vault.TestCluster, logical.SystemView) {
|
||||||
@@ -151,6 +153,8 @@ func TestBackend_config_connection(t *testing.T) {
|
|||||||
config := logical.TestBackendConfig()
|
config := logical.TestBackendConfig()
|
||||||
config.StorageView = &logical.InmemStorage{}
|
config.StorageView = &logical.InmemStorage{}
|
||||||
config.System = sys
|
config.System = sys
|
||||||
|
eventSender := logical.NewMockEventSender()
|
||||||
|
config.EventsSender = eventSender
|
||||||
lb, err := Factory(context.Background(), config)
|
lb, err := Factory(context.Background(), config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -329,6 +333,16 @@ func TestBackend_config_connection(t *testing.T) {
|
|||||||
if key != "plugin-test" {
|
if key != "plugin-test" {
|
||||||
t.Fatalf("bad key: %q", key)
|
t.Fatalf("bad key: %q", key)
|
||||||
}
|
}
|
||||||
|
assert.Equal(t, 3, len(eventSender.Events))
|
||||||
|
assert.Equal(t, "database/config-write", string(eventSender.Events[0].Type))
|
||||||
|
assert.Equal(t, "config/plugin-test", eventSender.Events[0].Event.Metadata.AsMap()["path"])
|
||||||
|
assert.Equal(t, "plugin-test", eventSender.Events[0].Event.Metadata.AsMap()["name"])
|
||||||
|
assert.Equal(t, "database/config-write", string(eventSender.Events[1].Type))
|
||||||
|
assert.Equal(t, "config/plugin-test", eventSender.Events[1].Event.Metadata.AsMap()["path"])
|
||||||
|
assert.Equal(t, "plugin-test", eventSender.Events[1].Event.Metadata.AsMap()["name"])
|
||||||
|
assert.Equal(t, "database/config-write", string(eventSender.Events[2].Type))
|
||||||
|
assert.Equal(t, "config/plugin-test", eventSender.Events[2].Event.Metadata.AsMap()["path"])
|
||||||
|
assert.Equal(t, "plugin-test", eventSender.Events[2].Event.Metadata.AsMap()["name"])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBackend_BadConnectionString(t *testing.T) {
|
func TestBackend_BadConnectionString(t *testing.T) {
|
||||||
@@ -387,6 +401,8 @@ func TestBackend_basic(t *testing.T) {
|
|||||||
config := logical.TestBackendConfig()
|
config := logical.TestBackendConfig()
|
||||||
config.StorageView = &logical.InmemStorage{}
|
config.StorageView = &logical.InmemStorage{}
|
||||||
config.System = sys
|
config.System = sys
|
||||||
|
eventSender := logical.NewMockEventSender()
|
||||||
|
config.EventsSender = eventSender
|
||||||
|
|
||||||
b, err := Factory(context.Background(), config)
|
b, err := Factory(context.Background(), config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -585,6 +601,23 @@ func TestBackend_basic(t *testing.T) {
|
|||||||
t.Fatalf("Creds should not exist")
|
t.Fatalf("Creds should not exist")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
assert.Equal(t, 9, len(eventSender.Events))
|
||||||
|
|
||||||
|
assertEvent := func(t *testing.T, typ, name, path string) {
|
||||||
|
t.Helper()
|
||||||
|
assert.Equal(t, typ, string(eventSender.Events[0].Type))
|
||||||
|
assert.Equal(t, name, eventSender.Events[0].Event.Metadata.AsMap()["name"])
|
||||||
|
assert.Equal(t, path, eventSender.Events[0].Event.Metadata.AsMap()["path"])
|
||||||
|
eventSender.Events = slices.Delete(eventSender.Events, 0, 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
assertEvent(t, "database/config-write", "plugin-test", "config/plugin-test")
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
assertEvent(t, "database/role-update", "plugin-role-test", "roles/plugin-role-test")
|
||||||
|
assertEvent(t, "database/creds-create", "plugin-role-test", "creds/plugin-role-test")
|
||||||
|
}
|
||||||
|
assertEvent(t, "database/creds-create", "plugin-role-test", "creds/plugin-role-test")
|
||||||
|
assertEvent(t, "database/role-delete", "plugin-role-test", "roles/plugin-role-test")
|
||||||
}
|
}
|
||||||
|
|
||||||
// singletonDBFactory allows us to reach into the internals of a databaseBackend
|
// singletonDBFactory allows us to reach into the internals of a databaseBackend
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ func (b *databaseBackend) pathConnectionReset() framework.OperationFunc {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.dbEvent(ctx, "reset", req.Path, name, false)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -196,7 +197,7 @@ func (b *databaseBackend) reloadPlugin() framework.OperationFunc {
|
|||||||
if len(reloaded) == 0 {
|
if len(reloaded) == 0 {
|
||||||
resp.AddWarning(fmt.Sprintf("no connections were found with plugin_name %q", pluginName))
|
resp.AddWarning(fmt.Sprintf("no connections were found with plugin_name %q", pluginName))
|
||||||
}
|
}
|
||||||
|
b.dbEvent(ctx, "reload", req.Path, "", true, "plugin_name", pluginName)
|
||||||
return resp, nil
|
return resp, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -413,6 +414,7 @@ func (b *databaseBackend) connectionDeleteHandler() framework.OperationFunc {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.dbEvent(ctx, "config-delete", req.Path, name, true)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -559,6 +561,7 @@ func (b *databaseBackend) connectionWriteHandler() framework.OperationFunc {
|
|||||||
"Vault (or the sdk if using a custom plugin) to gain password policy support", config.PluginName))
|
"Vault (or the sdk if using a custom plugin) to gain password policy support", config.PluginName))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.dbEvent(ctx, "config-write", req.Path, name, true)
|
||||||
if len(resp.Warnings) == 0 {
|
if len(resp.Warnings) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -67,8 +67,16 @@ func pathCredsCreate(b *databaseBackend) []*framework.Path {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *databaseBackend) pathCredsCreateRead() framework.OperationFunc {
|
func (b *databaseBackend) pathCredsCreateRead() framework.OperationFunc {
|
||||||
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (resp *logical.Response, err error) {
|
||||||
name := data.Get("name").(string)
|
name := data.Get("name").(string)
|
||||||
|
modified := false
|
||||||
|
defer func() {
|
||||||
|
if err == nil && (resp == nil || !resp.IsError()) {
|
||||||
|
b.dbEvent(ctx, "creds-create", req.Path, name, modified)
|
||||||
|
} else {
|
||||||
|
b.dbEvent(ctx, "creds-create-fail", req.Path, name, modified)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Get the role
|
// Get the role
|
||||||
role, err := b.Role(ctx, req.Storage, name)
|
role, err := b.Role(ctx, req.Storage, name)
|
||||||
@@ -202,6 +210,7 @@ func (b *databaseBackend) pathCredsCreateRead() framework.OperationFunc {
|
|||||||
b.CloseIfShutdown(dbi, err)
|
b.CloseIfShutdown(dbi, err)
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
modified = true
|
||||||
respData["username"] = newUserResp.Username
|
respData["username"] = newUserResp.Username
|
||||||
|
|
||||||
// Database plugins using the v4 interface generate and return the password.
|
// Database plugins using the v4 interface generate and return the password.
|
||||||
@@ -216,7 +225,7 @@ func (b *databaseBackend) pathCredsCreateRead() framework.OperationFunc {
|
|||||||
"db_name": role.DBName,
|
"db_name": role.DBName,
|
||||||
"revocation_statements": role.Statements.Revocation,
|
"revocation_statements": role.Statements.Revocation,
|
||||||
}
|
}
|
||||||
resp := b.Secret(SecretCredsType).Response(respData, internal)
|
resp = b.Secret(SecretCredsType).Response(respData, internal)
|
||||||
resp.Secret.TTL = role.DefaultTTL
|
resp.Secret.TTL = role.DefaultTTL
|
||||||
resp.Secret.MaxTTL = role.MaxTTL
|
resp.Secret.MaxTTL = role.MaxTTL
|
||||||
return resp, nil
|
return resp, nil
|
||||||
|
|||||||
@@ -238,11 +238,12 @@ func (b *databaseBackend) pathStaticRoleExistenceCheck(ctx context.Context, req
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *databaseBackend) pathRoleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
func (b *databaseBackend) pathRoleDelete(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
||||||
err := req.Storage.Delete(ctx, databaseRolePath+data.Get("name").(string))
|
name := data.Get("name").(string)
|
||||||
|
err := req.Storage.Delete(ctx, databaseRolePath+name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
b.dbEvent(ctx, "role-delete", req.Path, name, true)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -283,6 +284,7 @@ func (b *databaseBackend) pathStaticRoleDelete(ctx context.Context, req *logical
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.dbEvent(ctx, "static-role-delete", req.Path, name, true)
|
||||||
return nil, merr.ErrorOrNil()
|
return nil, merr.ErrorOrNil()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -498,6 +500,7 @@ func (b *databaseBackend) pathRoleCreateUpdate(ctx context.Context, req *logical
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
b.dbEvent(ctx, fmt.Sprintf("role-%s", req.Operation), req.Path, name, true)
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -696,7 +699,7 @@ func (b *databaseBackend) pathStaticRoleCreateUpdate(ctx context.Context, req *l
|
|||||||
if err := b.pushItem(item); err != nil {
|
if err := b.pushItem(item); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
b.dbEvent(ctx, fmt.Sprintf("static-role-%s", req.Operation), req.Path, name, true)
|
||||||
return response, nil
|
return response, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -75,8 +75,17 @@ func pathRotateRootCredentials(b *databaseBackend) []*framework.Path {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *databaseBackend) pathRotateRootCredentialsUpdate() framework.OperationFunc {
|
func (b *databaseBackend) pathRotateRootCredentialsUpdate() framework.OperationFunc {
|
||||||
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (resp *logical.Response, err error) {
|
||||||
name := data.Get("name").(string)
|
name := data.Get("name").(string)
|
||||||
|
modified := false
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
b.dbEvent(ctx, "rotate-root", req.Path, name, modified)
|
||||||
|
} else {
|
||||||
|
b.dbEvent(ctx, "rotate-root-fail", req.Path, name, modified)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return logical.ErrorResponse(respErrEmptyName), nil
|
return logical.ErrorResponse(respErrEmptyName), nil
|
||||||
}
|
}
|
||||||
@@ -159,6 +168,7 @@ func (b *databaseBackend) pathRotateRootCredentialsUpdate() framework.OperationF
|
|||||||
if newConfigDetails != nil {
|
if newConfigDetails != nil {
|
||||||
config.ConnectionDetails = newConfigDetails
|
config.ConnectionDetails = newConfigDetails
|
||||||
}
|
}
|
||||||
|
modified = true
|
||||||
|
|
||||||
// 1.12.0 and 1.12.1 stored builtin plugins in storage, but 1.12.2 reverted
|
// 1.12.0 and 1.12.1 stored builtin plugins in storage, but 1.12.2 reverted
|
||||||
// that, so clean up any pre-existing stored builtin versions on write.
|
// that, so clean up any pre-existing stored builtin versions on write.
|
||||||
@@ -179,8 +189,16 @@ func (b *databaseBackend) pathRotateRootCredentialsUpdate() framework.OperationF
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (b *databaseBackend) pathRotateRoleCredentialsUpdate() framework.OperationFunc {
|
func (b *databaseBackend) pathRotateRoleCredentialsUpdate() framework.OperationFunc {
|
||||||
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) {
|
return func(ctx context.Context, req *logical.Request, data *framework.FieldData) (_ *logical.Response, err error) {
|
||||||
name := data.Get("name").(string)
|
name := data.Get("name").(string)
|
||||||
|
modified := false
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
b.dbEvent(ctx, "rotate", req.Path, name, modified)
|
||||||
|
} else {
|
||||||
|
b.dbEvent(ctx, "rotate-fail", req.Path, name, modified)
|
||||||
|
}
|
||||||
|
}()
|
||||||
if name == "" {
|
if name == "" {
|
||||||
return logical.ErrorResponse("empty role name attribute given"), nil
|
return logical.ErrorResponse("empty role name attribute given"), nil
|
||||||
}
|
}
|
||||||
@@ -227,6 +245,7 @@ func (b *databaseBackend) pathRotateRoleCredentialsUpdate() framework.OperationF
|
|||||||
item.Priority = role.StaticAccount.NextRotationTimeFromInput(resp.RotationTime).Unix()
|
item.Priority = role.StaticAccount.NextRotationTimeFromInput(resp.RotationTime).Unix()
|
||||||
// Clear any stored WAL ID as we must have successfully deleted our WAL to get here.
|
// Clear any stored WAL ID as we must have successfully deleted our WAL to get here.
|
||||||
item.Value = ""
|
item.Value = ""
|
||||||
|
modified = true
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add their rotation to the queue
|
// Add their rotation to the queue
|
||||||
|
|||||||
@@ -252,6 +252,16 @@ func (b *databaseBackend) rotateCredential(ctx context.Context, s logical.Storag
|
|||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// send an event indicating if the rotation was a success or failure
|
||||||
|
rotated := false
|
||||||
|
defer func() {
|
||||||
|
if rotated {
|
||||||
|
b.dbEvent(ctx, "rotate", "", roleName, true)
|
||||||
|
} else {
|
||||||
|
b.dbEvent(ctx, "rotate-fail", "", roleName, false)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// If there is a WAL entry related to this Role, the corresponding WAL ID
|
// If there is a WAL entry related to this Role, the corresponding WAL ID
|
||||||
// should be stored in the Item's Value field.
|
// should be stored in the Item's Value field.
|
||||||
if walID, ok := item.Value.(string); ok {
|
if walID, ok := item.Value.(string); ok {
|
||||||
@@ -291,6 +301,7 @@ func (b *databaseBackend) rotateCredential(ctx context.Context, s logical.Storag
|
|||||||
if err := b.pushItem(item); err != nil {
|
if err := b.pushItem(item); err != nil {
|
||||||
logger.Warn("unable to push item on to queue", "error", err)
|
logger.Warn("unable to push item on to queue", "error", err)
|
||||||
}
|
}
|
||||||
|
rotated = true
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -350,10 +361,19 @@ type setStaticAccountOutput struct {
|
|||||||
//
|
//
|
||||||
// This method does not perform any operations on the priority queue. Those
|
// This method does not perform any operations on the priority queue. Those
|
||||||
// tasks must be handled outside of this method.
|
// tasks must be handled outside of this method.
|
||||||
func (b *databaseBackend) setStaticAccount(ctx context.Context, s logical.Storage, input *setStaticAccountInput) (*setStaticAccountOutput, error) {
|
func (b *databaseBackend) setStaticAccount(ctx context.Context, s logical.Storage, input *setStaticAccountInput) (_ *setStaticAccountOutput, err error) {
|
||||||
if input == nil || input.Role == nil || input.RoleName == "" {
|
if input == nil || input.Role == nil || input.RoleName == "" {
|
||||||
return nil, errors.New("input was empty when attempting to set credentials for static account")
|
return nil, errors.New("input was empty when attempting to set credentials for static account")
|
||||||
}
|
}
|
||||||
|
modified := false
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
b.dbEvent(ctx, "static-creds-create", "", input.RoleName, modified)
|
||||||
|
} else {
|
||||||
|
b.dbEvent(ctx, "static-creds-create-fail", "", input.RoleName, modified)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
// Re-use WAL ID if present, otherwise PUT a new WAL
|
// Re-use WAL ID if present, otherwise PUT a new WAL
|
||||||
output := &setStaticAccountOutput{WALID: input.WALID}
|
output := &setStaticAccountOutput{WALID: input.WALID}
|
||||||
|
|
||||||
@@ -507,6 +527,7 @@ func (b *databaseBackend) setStaticAccount(ctx context.Context, s logical.Storag
|
|||||||
b.CloseIfShutdown(dbi, err)
|
b.CloseIfShutdown(dbi, err)
|
||||||
return output, fmt.Errorf("error setting credentials: %w", err)
|
return output, fmt.Errorf("error setting credentials: %w", err)
|
||||||
}
|
}
|
||||||
|
modified = true
|
||||||
|
|
||||||
// Store updated role information
|
// Store updated role information
|
||||||
// lvr is the known LastVaultRotation
|
// lvr is the known LastVaultRotation
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import (
|
|||||||
"github.com/hashicorp/vault/sdk/queue"
|
"github.com/hashicorp/vault/sdk/queue"
|
||||||
_ "github.com/jackc/pgx/v4/stdlib"
|
_ "github.com/jackc/pgx/v4/stdlib"
|
||||||
"github.com/robfig/cron/v3"
|
"github.com/robfig/cron/v3"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/mock"
|
"github.com/stretchr/testify/mock"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
mongodbatlasapi "go.mongodb.org/atlas/mongodbatlas"
|
mongodbatlasapi "go.mongodb.org/atlas/mongodbatlas"
|
||||||
@@ -257,6 +258,8 @@ func TestBackend_StaticRole_Rotation_Schedule_ErrorRecover(t *testing.T) {
|
|||||||
config := logical.TestBackendConfig()
|
config := logical.TestBackendConfig()
|
||||||
config.StorageView = &logical.InmemStorage{}
|
config.StorageView = &logical.InmemStorage{}
|
||||||
config.System = sys
|
config.System = sys
|
||||||
|
eventSender := logical.NewMockEventSender()
|
||||||
|
config.EventsSender = eventSender
|
||||||
|
|
||||||
lb, err := Factory(context.Background(), config)
|
lb, err := Factory(context.Background(), config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -382,7 +385,6 @@ func TestBackend_StaticRole_Rotation_Schedule_ErrorRecover(t *testing.T) {
|
|||||||
// should match because rotations should not occur outside the rotation window
|
// should match because rotations should not occur outside the rotation window
|
||||||
t.Fatalf("expected passwords to match, got (%s)", checkPassword)
|
t.Fatalf("expected passwords to match, got (%s)", checkPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Verify new username/password
|
// Verify new username/password
|
||||||
verifyPgConn(t, username, checkPassword, connURL)
|
verifyPgConn(t, username, checkPassword, connURL)
|
||||||
|
|
||||||
@@ -409,6 +411,29 @@ func TestBackend_StaticRole_Rotation_Schedule_ErrorRecover(t *testing.T) {
|
|||||||
|
|
||||||
// Verify new username/password
|
// Verify new username/password
|
||||||
verifyPgConn(t, username, checkPassword, connURL)
|
verifyPgConn(t, username, checkPassword, connURL)
|
||||||
|
|
||||||
|
eventSender.Stop() // avoid race detector
|
||||||
|
// check that we got a successful rotation event
|
||||||
|
if len(eventSender.Events) == 0 {
|
||||||
|
t.Fatal("Expected to have some events but got none")
|
||||||
|
}
|
||||||
|
// check that we got a rotate-fail event
|
||||||
|
found := false
|
||||||
|
for _, event := range eventSender.Events {
|
||||||
|
if string(event.Type) == "database/rotate-fail" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found)
|
||||||
|
found = false
|
||||||
|
for _, event := range eventSender.Events {
|
||||||
|
if string(event.Type) == "database/rotate" {
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
assert.True(t, found)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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
|
||||||
|
|||||||
3
changelog/24718.txt
Normal file
3
changelog/24718.txt
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
```release-note:feature
|
||||||
|
database: Emit event notifications
|
||||||
|
```
|
||||||
46
sdk/logical/events_mock.go
Normal file
46
sdk/logical/events_mock.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package logical
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
// MockEventSender is a simple implementation of logical.EventSender that simply stores whatever events it receives,
|
||||||
|
// meant to be used in testing. It is thread-safe.
|
||||||
|
type MockEventSender struct {
|
||||||
|
sync.Mutex
|
||||||
|
Events []MockEvent
|
||||||
|
Stopped bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// MockEvent is a container for an event type + event pair.
|
||||||
|
type MockEvent struct {
|
||||||
|
Type EventType
|
||||||
|
Event *EventData
|
||||||
|
}
|
||||||
|
|
||||||
|
// SendEvent implements the logical.EventSender interface.
|
||||||
|
func (m *MockEventSender) SendEvent(_ context.Context, eventType EventType, event *EventData) error {
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
if !m.Stopped {
|
||||||
|
m.Events = append(m.Events, MockEvent{Type: eventType, Event: event})
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MockEventSender) Stop() {
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
m.Stopped = true
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ EventSender = (*MockEventSender)(nil)
|
||||||
|
|
||||||
|
// NewMockEventSender returns a new MockEventSender ready to be used.
|
||||||
|
func NewMockEventSender() *MockEventSender {
|
||||||
|
return &MockEventSender{}
|
||||||
|
}
|
||||||
@@ -25,7 +25,24 @@ additional `metadata` field.
|
|||||||
The following events are currently generated by Vault and its builtin plugins automatically:
|
The following events are currently generated by Vault and its builtin plugins automatically:
|
||||||
|
|
||||||
| Plugin | Event Type | Metadata | Vault version |
|
| Plugin | Event Type | Metadata | Vault version |
|
||||||
| ------ | ----------------------- | -------------------------------------------- | ------------- |
|
| -------- | ------------------------------------ | ---------------------------------------------- | ------------- |
|
||||||
|
| database | `database/config-delete` | `modified`, `operation`, `path`, `name` | 1.16 |
|
||||||
|
| database | `database/config-write` | `modified`, `operation`, `path`, `name` | 1.16 |
|
||||||
|
| database | `database/creds-create` | `modified`, `operation`, `path`, `name` | 1.16 |
|
||||||
|
| database | `database/reload` | `modified`, `operation`, `path`, `plugin_name` | 1.16 |
|
||||||
|
| database | `database/reset` | `modified`, `operation`, `path`, `name` | 1.16 |
|
||||||
|
| database | `database/role-create` | `modified`, `operation`, `path`, `name` | 1.16 |
|
||||||
|
| database | `database/role-delete` | `modified`, `operation`, `path`, `name` | 1.16 |
|
||||||
|
| database | `database/role-update` | `modified`, `operation`, `path`, `name` | 1.16 |
|
||||||
|
| database | `database/root-rotate-fail` | `modified`, `operation`, `path`, `name` | 1.16 |
|
||||||
|
| database | `database/root-rotate` | `modified`, `operation`, `path`, `name` | 1.16 |
|
||||||
|
| database | `database/rotate-fail` | `modified`, `operation`, `path`, `name` | 1.16 |
|
||||||
|
| database | `database/rotate` | `modified`, `operation`, `path`, `name` | 1.16 |
|
||||||
|
| database | `database/static-creds-create-fail` | `modified`, `operation`, `path`, `name` | 1.16 |
|
||||||
|
| database | `database/static-creds-create` | `modified`, `operation`, `path`, `name` | 1.16 |
|
||||||
|
| database | `database/static-role-create` | `modified`, `operation`, `path`, `name` | 1.16 |
|
||||||
|
| database | `database/static-role-delete` | `modified`, `operation`, `path`, `name` | 1.16 |
|
||||||
|
| database | `database/static-role-update` | `modified`, `operation`, `path`, `name` | 1.16 |
|
||||||
| kv | `kv-v1/delete` | `modified`, `operation`, `path` | 1.13 |
|
| kv | `kv-v1/delete` | `modified`, `operation`, `path` | 1.13 |
|
||||||
| kv | `kv-v1/write` | `data_path`, `modified`, `operation`, `path` | 1.13 |
|
| kv | `kv-v1/write` | `data_path`, `modified`, `operation`, `path` | 1.13 |
|
||||||
| kv | `kv-v2/config-write` | `data_path`, `modified`, `operation`, `path` | 1.13 |
|
| kv | `kv-v2/config-write` | `data_path`, `modified`, `operation`, `path` | 1.13 |
|
||||||
|
|||||||
Reference in New Issue
Block a user