mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 02:02:43 +00:00
VAULT-24449: Migrate 'audit filtering' feature to Enterprise (#25711)
* Fix an audit filtering test Move configureFilterNode to ent-specific files and add non-ent stubs Update tests for file audit devices Add tests for socket audit device Add syslog audit device tests Prevent enabling an audit device with 'enterprise only' options in CE Check enterprise only audit options on db load (unseal) newAuditBackend test * Fix assignment of audit broker to core during audit setup * Removed Enterprise only audit feature tests (maintained in Enterprise repo) * Replace enterprise filtering tests with ones for CE * Remove redundant temp file creation calls in CE tests for filtering --------- Co-authored-by: Kuba Wieczorek <kuba.wieczorek@hashicorp.com>
This commit is contained in:
@@ -223,31 +223,6 @@ func formatterConfig(config map[string]string) (audit.FormatterConfig, error) {
|
||||
return audit.NewFormatterConfig(opts...)
|
||||
}
|
||||
|
||||
// configureFilterNode is used to configure a filter node and associated ID on the Backend.
|
||||
func (b *Backend) configureFilterNode(filter string) error {
|
||||
const op = "file.(Backend).configureFilterNode"
|
||||
|
||||
filter = strings.TrimSpace(filter)
|
||||
if filter == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
filterNodeID, err := event.GenerateNodeID()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: error generating random NodeID for filter node: %w", op, err)
|
||||
}
|
||||
|
||||
filterNode, err := audit.NewEntryFilter(filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: error creating filter node: %w", op, err)
|
||||
}
|
||||
|
||||
b.nodeIDList = append(b.nodeIDList, filterNodeID)
|
||||
b.nodeMap[filterNodeID] = filterNode
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// configureFormatterNode is used to configure a formatter node and associated ID on the Backend.
|
||||
func (b *Backend) configureFormatterNode(name string, formatConfig audit.FormatterConfig, logger hclog.Logger, opts ...audit.Option) error {
|
||||
const op = "file.(Backend).configureFormatterNode"
|
||||
|
||||
11
builtin/audit/file/backend_filter_node.go
Normal file
11
builtin/audit/file/backend_filter_node.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !enterprise
|
||||
|
||||
package file
|
||||
|
||||
// configureFilterNode is used to configure a filter node and associated ID on the Backend.
|
||||
func (b *Backend) configureFilterNode(_ string) error {
|
||||
return nil
|
||||
}
|
||||
99
builtin/audit/file/backend_filter_node_test.go
Normal file
99
builtin/audit/file/backend_filter_node_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !enterprise
|
||||
|
||||
package file
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/eventlogger"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/audit"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestBackend_configureFilterNode ensures that configureFilterNode handles various
|
||||
// filter values as expected. Empty (including whitespace) strings should return
|
||||
// no error but skip configuration of the node.
|
||||
// NOTE: Audit filtering is an Enterprise feature and behaves differently in the
|
||||
// community edition of Vault.
|
||||
func TestBackend_configureFilterNode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := map[string]struct {
|
||||
filter string
|
||||
}{
|
||||
"happy": {
|
||||
filter: "operation == update",
|
||||
},
|
||||
"empty": {
|
||||
filter: "",
|
||||
},
|
||||
"spacey": {
|
||||
filter: " ",
|
||||
},
|
||||
"bad": {
|
||||
filter: "___qwerty",
|
||||
},
|
||||
"unsupported-field": {
|
||||
filter: "foo == bar",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
name := name
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &Backend{
|
||||
nodeIDList: []eventlogger.NodeID{},
|
||||
nodeMap: map[eventlogger.NodeID]eventlogger.Node{},
|
||||
}
|
||||
|
||||
err := b.configureFilterNode(tc.filter)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, b.nodeIDList, 0)
|
||||
require.Len(t, b.nodeMap, 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackend_configureFilterFormatterSink ensures that configuring all three
|
||||
// types of nodes on a Backend works as expected, i.e. we have only formatter and sink
|
||||
// nodes at the end and nothing gets overwritten. The order of calls influences the
|
||||
// slice of IDs on the Backend.
|
||||
// NOTE: Audit filtering is an Enterprise feature and behaves differently in the
|
||||
// community edition of Vault.
|
||||
func TestBackend_configureFilterFormatterSink(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &Backend{
|
||||
nodeIDList: []eventlogger.NodeID{},
|
||||
nodeMap: map[eventlogger.NodeID]eventlogger.Node{},
|
||||
}
|
||||
|
||||
formatConfig, err := audit.NewFormatterConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureFilterNode("path == bar")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureFormatterNode("juan", formatConfig, hclog.NewNullLogger())
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureSinkNode("foo", "/tmp/foo", "0777", "json")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, b.nodeIDList, 2)
|
||||
require.Len(t, b.nodeMap, 2)
|
||||
|
||||
id := b.nodeIDList[0]
|
||||
node := b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeFormatter, node.Type())
|
||||
|
||||
id = b.nodeIDList[1]
|
||||
node = b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeSink, node.Type())
|
||||
}
|
||||
@@ -243,75 +243,6 @@ func TestBackend_formatterConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackend_configureFilterNode ensures that configureFilterNode handles various
|
||||
// filter values as expected. Empty (including whitespace) strings should return
|
||||
// no error but skip configuration of the node.
|
||||
func TestBackend_configureFilterNode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := map[string]struct {
|
||||
filter string
|
||||
shouldSkipNode bool
|
||||
wantErr bool
|
||||
expectedErrorMsg string
|
||||
}{
|
||||
"happy": {
|
||||
filter: "operation == update",
|
||||
},
|
||||
"empty": {
|
||||
filter: "",
|
||||
shouldSkipNode: true,
|
||||
},
|
||||
"spacey": {
|
||||
filter: " ",
|
||||
shouldSkipNode: true,
|
||||
},
|
||||
"bad": {
|
||||
filter: "___qwerty",
|
||||
wantErr: true,
|
||||
expectedErrorMsg: "file.(Backend).configureFilterNode: error creating filter node: audit.NewEntryFilter: cannot create new audit filter",
|
||||
},
|
||||
"unsupported-field": {
|
||||
filter: "foo == bar",
|
||||
wantErr: true,
|
||||
expectedErrorMsg: "filter references an unsupported field: foo == bar",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
name := name
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &Backend{
|
||||
nodeIDList: []eventlogger.NodeID{},
|
||||
nodeMap: map[eventlogger.NodeID]eventlogger.Node{},
|
||||
}
|
||||
|
||||
err := b.configureFilterNode(tc.filter)
|
||||
|
||||
switch {
|
||||
case tc.wantErr:
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, tc.expectedErrorMsg)
|
||||
require.Len(t, b.nodeIDList, 0)
|
||||
require.Len(t, b.nodeMap, 0)
|
||||
case tc.shouldSkipNode:
|
||||
require.NoError(t, err)
|
||||
require.Len(t, b.nodeIDList, 0)
|
||||
require.Len(t, b.nodeMap, 0)
|
||||
default:
|
||||
require.NoError(t, err)
|
||||
require.Len(t, b.nodeIDList, 1)
|
||||
require.Len(t, b.nodeMap, 1)
|
||||
id := b.nodeIDList[0]
|
||||
node := b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeFilter, node.Type())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackend_configureFormatterNode ensures that configureFormatterNode
|
||||
// populates the nodeIDList and nodeMap on Backend when given valid formatConfig.
|
||||
func TestBackend_configureFormatterNode(t *testing.T) {
|
||||
@@ -472,46 +403,6 @@ func TestBackend_configureSinkNode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackend_configureFilterFormatterSink ensures that configuring all three
|
||||
// types of nodes on a Backend works as expected, i.e. we have all three nodes
|
||||
// at the end and nothing gets overwritten. The order of calls influences the
|
||||
// slice of IDs on the Backend.
|
||||
func TestBackend_configureFilterFormatterSink(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &Backend{
|
||||
nodeIDList: []eventlogger.NodeID{},
|
||||
nodeMap: map[eventlogger.NodeID]eventlogger.Node{},
|
||||
}
|
||||
|
||||
formatConfig, err := audit.NewFormatterConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureFilterNode("path == bar")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureFormatterNode("juan", formatConfig, hclog.NewNullLogger())
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureSinkNode("foo", "/tmp/foo", "0777", "json")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, b.nodeIDList, 3)
|
||||
require.Len(t, b.nodeMap, 3)
|
||||
|
||||
id := b.nodeIDList[0]
|
||||
node := b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeFilter, node.Type())
|
||||
|
||||
id = b.nodeIDList[1]
|
||||
node = b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeFormatter, node.Type())
|
||||
|
||||
id = b.nodeIDList[2]
|
||||
node = b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeSink, node.Type())
|
||||
}
|
||||
|
||||
// TestBackend_Factory_Conf is used to ensure that any configuration which is
|
||||
// supplied, is validated and tested.
|
||||
func TestBackend_Factory_Conf(t *testing.T) {
|
||||
|
||||
@@ -205,31 +205,6 @@ func formatterConfig(config map[string]string) (audit.FormatterConfig, error) {
|
||||
return audit.NewFormatterConfig(cfgOpts...)
|
||||
}
|
||||
|
||||
// configureFilterNode is used to configure a filter node and associated ID on the Backend.
|
||||
func (b *Backend) configureFilterNode(filter string) error {
|
||||
const op = "socket.(Backend).configureFilterNode"
|
||||
|
||||
filter = strings.TrimSpace(filter)
|
||||
if filter == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
filterNodeID, err := event.GenerateNodeID()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: error generating random NodeID for filter node: %w", op, err)
|
||||
}
|
||||
|
||||
filterNode, err := audit.NewEntryFilter(filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: error creating filter node: %w", op, err)
|
||||
}
|
||||
|
||||
b.nodeIDList = append(b.nodeIDList, filterNodeID)
|
||||
b.nodeMap[filterNodeID] = filterNode
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// configureFormatterNode is used to configure a formatter node and associated ID on the Backend.
|
||||
func (b *Backend) configureFormatterNode(name string, formatConfig audit.FormatterConfig, logger hclog.Logger, opts ...audit.Option) error {
|
||||
const op = "socket.(Backend).configureFormatterNode"
|
||||
|
||||
11
builtin/audit/socket/backend_filter_node.go
Normal file
11
builtin/audit/socket/backend_filter_node.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !enterprise
|
||||
|
||||
package socket
|
||||
|
||||
// configureFilterNode is used to configure a filter node and associated ID on the Backend.
|
||||
func (b *Backend) configureFilterNode(_ string) error {
|
||||
return nil
|
||||
}
|
||||
99
builtin/audit/socket/backend_filter_node_test.go
Normal file
99
builtin/audit/socket/backend_filter_node_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !enterprise
|
||||
|
||||
package socket
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/eventlogger"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/audit"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestBackend_configureFilterNode ensures that configureFilterNode handles various
|
||||
// filter values as expected. Empty (including whitespace) strings should return
|
||||
// no error but skip configuration of the node.
|
||||
// NOTE: Audit filtering is an Enterprise feature and behaves differently in the
|
||||
// community edition of Vault.
|
||||
func TestBackend_configureFilterNode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := map[string]struct {
|
||||
filter string
|
||||
}{
|
||||
"happy": {
|
||||
filter: "operation == update",
|
||||
},
|
||||
"empty": {
|
||||
filter: "",
|
||||
},
|
||||
"spacey": {
|
||||
filter: " ",
|
||||
},
|
||||
"bad": {
|
||||
filter: "___qwerty",
|
||||
},
|
||||
"unsupported-field": {
|
||||
filter: "foo == bar",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
name := name
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &Backend{
|
||||
nodeIDList: []eventlogger.NodeID{},
|
||||
nodeMap: map[eventlogger.NodeID]eventlogger.Node{},
|
||||
}
|
||||
|
||||
err := b.configureFilterNode(tc.filter)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, b.nodeIDList, 0)
|
||||
require.Len(t, b.nodeMap, 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackend_configureFilterFormatterSink ensures that configuring all three
|
||||
// types of nodes on a Backend works as expected, i.e. we have only formatter and sink
|
||||
// nodes at the end and nothing gets overwritten. The order of calls influences the
|
||||
// slice of IDs on the Backend.
|
||||
// NOTE: Audit filtering is an Enterprise feature and behaves differently in the
|
||||
// community edition of Vault.
|
||||
func TestBackend_configureFilterFormatterSink(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &Backend{
|
||||
nodeIDList: []eventlogger.NodeID{},
|
||||
nodeMap: map[eventlogger.NodeID]eventlogger.Node{},
|
||||
}
|
||||
|
||||
formatConfig, err := audit.NewFormatterConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureFilterNode("path == bar")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureFormatterNode("juan", formatConfig, hclog.NewNullLogger())
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureSinkNode("foo", "https://hashicorp.com", "json")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, b.nodeIDList, 2)
|
||||
require.Len(t, b.nodeMap, 2)
|
||||
|
||||
id := b.nodeIDList[0]
|
||||
node := b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeFormatter, node.Type())
|
||||
|
||||
id = b.nodeIDList[1]
|
||||
node = b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeSink, node.Type())
|
||||
}
|
||||
@@ -115,75 +115,6 @@ func TestBackend_formatterConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackend_configureFilterNode ensures that configureFilterNode handles various
|
||||
// filter values as expected. Empty (including whitespace) strings should return
|
||||
// no error but skip configuration of the node.
|
||||
func TestBackend_configureFilterNode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := map[string]struct {
|
||||
filter string
|
||||
shouldSkipNode bool
|
||||
wantErr bool
|
||||
expectedErrorMsg string
|
||||
}{
|
||||
"happy": {
|
||||
filter: "mount_point == \"/auth/token\"",
|
||||
},
|
||||
"empty": {
|
||||
filter: "",
|
||||
shouldSkipNode: true,
|
||||
},
|
||||
"spacey": {
|
||||
filter: " ",
|
||||
shouldSkipNode: true,
|
||||
},
|
||||
"bad": {
|
||||
filter: "___qwerty",
|
||||
wantErr: true,
|
||||
expectedErrorMsg: "socket.(Backend).configureFilterNode: error creating filter node: audit.NewEntryFilter: cannot create new audit filter",
|
||||
},
|
||||
"unsupported-field": {
|
||||
filter: "foo == bar",
|
||||
wantErr: true,
|
||||
expectedErrorMsg: "filter references an unsupported field: foo == bar",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
name := name
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &Backend{
|
||||
nodeIDList: []eventlogger.NodeID{},
|
||||
nodeMap: map[eventlogger.NodeID]eventlogger.Node{},
|
||||
}
|
||||
|
||||
err := b.configureFilterNode(tc.filter)
|
||||
|
||||
switch {
|
||||
case tc.wantErr:
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, tc.expectedErrorMsg)
|
||||
require.Len(t, b.nodeIDList, 0)
|
||||
require.Len(t, b.nodeMap, 0)
|
||||
case tc.shouldSkipNode:
|
||||
require.NoError(t, err)
|
||||
require.Len(t, b.nodeIDList, 0)
|
||||
require.Len(t, b.nodeMap, 0)
|
||||
default:
|
||||
require.NoError(t, err)
|
||||
require.Len(t, b.nodeIDList, 1)
|
||||
require.Len(t, b.nodeMap, 1)
|
||||
id := b.nodeIDList[0]
|
||||
node := b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeFilter, node.Type())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackend_configureFormatterNode ensures that configureFormatterNode
|
||||
// populates the nodeIDList and nodeMap on Backend when given valid formatConfig.
|
||||
func TestBackend_configureFormatterNode(t *testing.T) {
|
||||
@@ -300,46 +231,6 @@ func TestBackend_configureSinkNode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackend_configureFilterFormatterSink ensures that configuring all three
|
||||
// types of nodes on a Backend works as expected, i.e. we have all three nodes
|
||||
// at the end and nothing gets overwritten. The order of calls influences the
|
||||
// slice of IDs on the Backend.
|
||||
func TestBackend_configureFilterFormatterSink(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &Backend{
|
||||
nodeIDList: []eventlogger.NodeID{},
|
||||
nodeMap: map[eventlogger.NodeID]eventlogger.Node{},
|
||||
}
|
||||
|
||||
formatConfig, err := audit.NewFormatterConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureFilterNode("mount_type == kv")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureFormatterNode("juan", formatConfig, hclog.NewNullLogger())
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureSinkNode("foo", "https://hashicorp.com", "json")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, b.nodeIDList, 3)
|
||||
require.Len(t, b.nodeMap, 3)
|
||||
|
||||
id := b.nodeIDList[0]
|
||||
node := b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeFilter, node.Type())
|
||||
|
||||
id = b.nodeIDList[1]
|
||||
node = b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeFormatter, node.Type())
|
||||
|
||||
id = b.nodeIDList[2]
|
||||
node = b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeSink, node.Type())
|
||||
}
|
||||
|
||||
// TestBackend_Factory_Conf is used to ensure that any configuration which is
|
||||
// supplied, is validated and tested.
|
||||
func TestBackend_Factory_Conf(t *testing.T) {
|
||||
|
||||
@@ -196,31 +196,6 @@ func formatterConfig(config map[string]string) (audit.FormatterConfig, error) {
|
||||
return audit.NewFormatterConfig(opts...)
|
||||
}
|
||||
|
||||
// configureFilterNode is used to configure a filter node and associated ID on the Backend.
|
||||
func (b *Backend) configureFilterNode(filter string) error {
|
||||
const op = "syslog.(Backend).configureFilterNode"
|
||||
|
||||
filter = strings.TrimSpace(filter)
|
||||
if filter == "" {
|
||||
return nil
|
||||
}
|
||||
|
||||
filterNodeID, err := event.GenerateNodeID()
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: error generating random NodeID for filter node: %w", op, err)
|
||||
}
|
||||
|
||||
filterNode, err := audit.NewEntryFilter(filter)
|
||||
if err != nil {
|
||||
return fmt.Errorf("%s: error creating filter node: %w", op, err)
|
||||
}
|
||||
|
||||
b.nodeIDList = append(b.nodeIDList, filterNodeID)
|
||||
b.nodeMap[filterNodeID] = filterNode
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// configureFormatterNode is used to configure a formatter node and associated ID on the Backend.
|
||||
func (b *Backend) configureFormatterNode(name string, formatConfig audit.FormatterConfig, logger hclog.Logger, opts ...audit.Option) error {
|
||||
const op = "syslog.(Backend).configureFormatterNode"
|
||||
|
||||
11
builtin/audit/syslog/backend_filter_node.go
Normal file
11
builtin/audit/syslog/backend_filter_node.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !enterprise
|
||||
|
||||
package syslog
|
||||
|
||||
// configureFilterNode is used to configure a filter node and associated ID on the Backend.
|
||||
func (b *Backend) configureFilterNode(_ string) error {
|
||||
return nil
|
||||
}
|
||||
99
builtin/audit/syslog/backend_filter_node_test.go
Normal file
99
builtin/audit/syslog/backend_filter_node_test.go
Normal file
@@ -0,0 +1,99 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !enterprise
|
||||
|
||||
package syslog
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/eventlogger"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/audit"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestBackend_configureFilterNode ensures that configureFilterNode handles various
|
||||
// filter values as expected. Empty (including whitespace) strings should return
|
||||
// no error but skip configuration of the node.
|
||||
// NOTE: Audit filtering is an Enterprise feature and behaves differently in the
|
||||
// community edition of Vault.
|
||||
func TestBackend_configureFilterNode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := map[string]struct {
|
||||
filter string
|
||||
}{
|
||||
"happy": {
|
||||
filter: "operation == update",
|
||||
},
|
||||
"empty": {
|
||||
filter: "",
|
||||
},
|
||||
"spacey": {
|
||||
filter: " ",
|
||||
},
|
||||
"bad": {
|
||||
filter: "___qwerty",
|
||||
},
|
||||
"unsupported-field": {
|
||||
filter: "foo == bar",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
name := name
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &Backend{
|
||||
nodeIDList: []eventlogger.NodeID{},
|
||||
nodeMap: map[eventlogger.NodeID]eventlogger.Node{},
|
||||
}
|
||||
|
||||
err := b.configureFilterNode(tc.filter)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, b.nodeIDList, 0)
|
||||
require.Len(t, b.nodeMap, 0)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackend_configureFilterFormatterSink ensures that configuring all three
|
||||
// types of nodes on a Backend works as expected, i.e. we have only formatter and sink
|
||||
// nodes at the end and nothing gets overwritten. The order of calls influences the
|
||||
// slice of IDs on the Backend.
|
||||
// NOTE: Audit filtering is an Enterprise feature and behaves differently in the
|
||||
// community edition of Vault.
|
||||
func TestBackend_configureFilterFormatterSink(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &Backend{
|
||||
nodeIDList: []eventlogger.NodeID{},
|
||||
nodeMap: map[eventlogger.NodeID]eventlogger.Node{},
|
||||
}
|
||||
|
||||
formatConfig, err := audit.NewFormatterConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureFilterNode("path == bar")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureFormatterNode("juan", formatConfig, hclog.NewNullLogger())
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureSinkNode("foo", "json")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, b.nodeIDList, 2)
|
||||
require.Len(t, b.nodeMap, 2)
|
||||
|
||||
id := b.nodeIDList[0]
|
||||
node := b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeFormatter, node.Type())
|
||||
|
||||
id = b.nodeIDList[1]
|
||||
node = b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeSink, node.Type())
|
||||
}
|
||||
@@ -7,9 +7,8 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/go-hclog"
|
||||
|
||||
"github.com/hashicorp/eventlogger"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/audit"
|
||||
"github.com/hashicorp/vault/internal/observability/event"
|
||||
"github.com/hashicorp/vault/sdk/helper/salt"
|
||||
@@ -116,75 +115,6 @@ func TestBackend_formatterConfig(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackend_configureFilterNode ensures that configureFilterNode handles various
|
||||
// filter values as expected. Empty (including whitespace) strings should return
|
||||
// no error but skip configuration of the node.
|
||||
func TestBackend_configureFilterNode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := map[string]struct {
|
||||
filter string
|
||||
shouldSkipNode bool
|
||||
wantErr bool
|
||||
expectedErrorMsg string
|
||||
}{
|
||||
"happy": {
|
||||
filter: "namespace == bar",
|
||||
},
|
||||
"empty": {
|
||||
filter: "",
|
||||
shouldSkipNode: true,
|
||||
},
|
||||
"spacey": {
|
||||
filter: " ",
|
||||
shouldSkipNode: true,
|
||||
},
|
||||
"bad": {
|
||||
filter: "___qwerty",
|
||||
wantErr: true,
|
||||
expectedErrorMsg: "syslog.(Backend).configureFilterNode: error creating filter node: audit.NewEntryFilter: cannot create new audit filter",
|
||||
},
|
||||
"unsupported-field": {
|
||||
filter: "foo == bar",
|
||||
wantErr: true,
|
||||
expectedErrorMsg: "filter references an unsupported field: foo == bar",
|
||||
},
|
||||
}
|
||||
for name, tc := range tests {
|
||||
name := name
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &Backend{
|
||||
nodeIDList: []eventlogger.NodeID{},
|
||||
nodeMap: map[eventlogger.NodeID]eventlogger.Node{},
|
||||
}
|
||||
|
||||
err := b.configureFilterNode(tc.filter)
|
||||
|
||||
switch {
|
||||
case tc.wantErr:
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, tc.expectedErrorMsg)
|
||||
require.Len(t, b.nodeIDList, 0)
|
||||
require.Len(t, b.nodeMap, 0)
|
||||
case tc.shouldSkipNode:
|
||||
require.NoError(t, err)
|
||||
require.Len(t, b.nodeIDList, 0)
|
||||
require.Len(t, b.nodeMap, 0)
|
||||
default:
|
||||
require.NoError(t, err)
|
||||
require.Len(t, b.nodeIDList, 1)
|
||||
require.Len(t, b.nodeMap, 1)
|
||||
id := b.nodeIDList[0]
|
||||
node := b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeFilter, node.Type())
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackend_configureFormatterNode ensures that configureFormatterNode
|
||||
// populates the nodeIDList and nodeMap on Backend when given valid formatConfig.
|
||||
func TestBackend_configureFormatterNode(t *testing.T) {
|
||||
@@ -283,46 +213,6 @@ func TestBackend_configureSinkNode(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBackend_configureFilterFormatterSink ensures that configuring all three
|
||||
// types of nodes on a Backend works as expected, i.e. we have all three nodes
|
||||
// at the end and nothing gets overwritten. The order of calls influences the
|
||||
// slice of IDs on the Backend.
|
||||
func TestBackend_configureFilterFormatterSink(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
b := &Backend{
|
||||
nodeIDList: []eventlogger.NodeID{},
|
||||
nodeMap: map[eventlogger.NodeID]eventlogger.Node{},
|
||||
}
|
||||
|
||||
formatConfig, err := audit.NewFormatterConfig()
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureFilterNode("mount_type == kv")
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureFormatterNode("juan", formatConfig, hclog.NewNullLogger())
|
||||
require.NoError(t, err)
|
||||
|
||||
err = b.configureSinkNode("foo", "json")
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, b.nodeIDList, 3)
|
||||
require.Len(t, b.nodeMap, 3)
|
||||
|
||||
id := b.nodeIDList[0]
|
||||
node := b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeFilter, node.Type())
|
||||
|
||||
id = b.nodeIDList[1]
|
||||
node = b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeFormatter, node.Type())
|
||||
|
||||
id = b.nodeIDList[2]
|
||||
node = b.nodeMap[id]
|
||||
require.Equal(t, eventlogger.NodeTypeSink, node.Type())
|
||||
}
|
||||
|
||||
// TestBackend_Factory_Conf is used to ensure that any configuration which is
|
||||
// supplied, is validated and tested.
|
||||
func TestBackend_Factory_Conf(t *testing.T) {
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
"github.com/hashicorp/go-secure-stdlib/parseutil"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/hashicorp/vault/audit"
|
||||
"github.com/hashicorp/vault/helper/constants"
|
||||
"github.com/hashicorp/vault/helper/namespace"
|
||||
"github.com/hashicorp/vault/sdk/helper/consts"
|
||||
"github.com/hashicorp/vault/sdk/helper/jsonutil"
|
||||
@@ -74,6 +75,11 @@ func (c *Core) enableAudit(ctx context.Context, entry *MountEntry, updateStorage
|
||||
return fmt.Errorf("backend path must be specified")
|
||||
}
|
||||
|
||||
// We can check early to ensure that non-Enterprise versions aren't trying to supply Enterprise only options.
|
||||
if hasInvalidAuditOptions(entry.Options) {
|
||||
return fmt.Errorf("enterprise-only options supplied")
|
||||
}
|
||||
|
||||
if fallbackRaw, ok := entry.Options["fallback"]; ok {
|
||||
fallback, err := parseutil.ParseBool(fallbackRaw)
|
||||
if err != nil {
|
||||
@@ -414,6 +420,7 @@ func (c *Core) setupAudits(ctx context.Context) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
c.auditBroker = broker
|
||||
|
||||
var successCount int
|
||||
|
||||
@@ -457,7 +464,6 @@ func (c *Core) setupAudits(ctx context.Context) error {
|
||||
return errLoadAuditFailed
|
||||
}
|
||||
|
||||
c.auditBroker = broker
|
||||
c.AddLogger(brokerLogger)
|
||||
return nil
|
||||
}
|
||||
@@ -507,6 +513,11 @@ func (c *Core) removeAuditReloadFunc(entry *MountEntry) {
|
||||
|
||||
// newAuditBackend is used to create and configure a new audit backend by name
|
||||
func (c *Core) newAuditBackend(ctx context.Context, entry *MountEntry, view logical.Storage, conf map[string]string) (audit.Backend, error) {
|
||||
// Ensure that non-Enterprise versions aren't trying to supply Enterprise only options.
|
||||
if hasInvalidAuditOptions(entry.Options) {
|
||||
return nil, fmt.Errorf("enterprise-only options supplied")
|
||||
}
|
||||
|
||||
f, ok := c.auditBackends[entry.Type]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unknown backend type: %q", entry.Type)
|
||||
@@ -617,3 +628,29 @@ func (g genericAuditor) AuditResponse(ctx context.Context, input *logical.LogInp
|
||||
logInput.Type = g.mountType + "-response"
|
||||
return g.c.auditBroker.LogResponse(ctx, &logInput)
|
||||
}
|
||||
|
||||
// hasInvalidAuditOptions is used to determine if a non-Enterprise version of Vault
|
||||
// is being used when supplying options that contain options exclusive to Enterprise.
|
||||
func hasInvalidAuditOptions(options map[string]string) bool {
|
||||
return !constants.IsEnterprise && hasEnterpriseAuditOptions(options)
|
||||
}
|
||||
|
||||
// hasValidEnterpriseAuditOptions is used to check if any of the options supplied
|
||||
// are only for use in the Enterprise version of Vault.
|
||||
func hasEnterpriseAuditOptions(options map[string]string) bool {
|
||||
const enterpriseAuditOptionFilter = "filter"
|
||||
const enterpriseAuditOptionFallback = "fallback"
|
||||
|
||||
enterpriseAuditOptions := []string{
|
||||
enterpriseAuditOptionFallback,
|
||||
enterpriseAuditOptionFilter,
|
||||
}
|
||||
|
||||
for _, o := range enterpriseAuditOptions {
|
||||
if _, ok := options[o]; ok {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -9,20 +9,20 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/eventlogger"
|
||||
"github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/vault/audit"
|
||||
"github.com/hashicorp/vault/builtin/audit/file"
|
||||
"github.com/hashicorp/vault/builtin/audit/syslog"
|
||||
"github.com/hashicorp/vault/helper/namespace"
|
||||
"github.com/hashicorp/vault/helper/testhelpers/corehelpers"
|
||||
"github.com/hashicorp/vault/internal/observability/event"
|
||||
"github.com/hashicorp/vault/sdk/helper/salt"
|
||||
"github.com/hashicorp/vault/sdk/logical"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// testAuditBackend will create an audit.Backend (which expects to use the eventlogger).
|
||||
// NOTE: this will create the backend, it does not care whether or not Enterprise
|
||||
// only options are in place.
|
||||
func testAuditBackend(t *testing.T, path string, config map[string]string) audit.Backend {
|
||||
t.Helper()
|
||||
|
||||
@@ -54,176 +54,6 @@ func testAuditBackend(t *testing.T, path string, config map[string]string) audit
|
||||
return be
|
||||
}
|
||||
|
||||
// TestAuditBroker_Register_SuccessThresholdSinks tests that we are able to
|
||||
// correctly identify what the required success threshold sinks value on the
|
||||
// eventlogger broker should be set to.
|
||||
// We expect:
|
||||
// * 0 for only filtered backends
|
||||
// * 1 for any other combination
|
||||
func TestAuditBroker_Register_SuccessThresholdSinks(t *testing.T) {
|
||||
t.Parallel()
|
||||
l := corehelpers.NewTestLogger(t)
|
||||
a, err := NewAuditBroker(l)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, a)
|
||||
|
||||
filterBackend := testAuditBackend(t, "b1-filter", map[string]string{"filter": "operation == create"})
|
||||
noFilterBackend := testAuditBackend(t, "b2-no-filter", map[string]string{})
|
||||
|
||||
// Should be set to 0 for required sinks (and not found, as we've never registered before).
|
||||
res, ok := a.broker.SuccessThresholdSinks(eventlogger.EventType(event.AuditType.String()))
|
||||
require.False(t, ok)
|
||||
require.Equal(t, 0, res)
|
||||
|
||||
// Register the filtered backend first, this shouldn't change the
|
||||
// success threshold sinks to 1 as we can't guarantee any device yet.
|
||||
err = a.Register("b1-filter", filterBackend, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check the SuccessThresholdSinks (we expect 0 still, but found).
|
||||
res, ok = a.broker.SuccessThresholdSinks(eventlogger.EventType(event.AuditType.String()))
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 0, res)
|
||||
|
||||
// Register the non-filtered backend second, this should mean we
|
||||
// can rely on guarantees from the broker again.
|
||||
err = a.Register("b2-no-filter", noFilterBackend, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check the SuccessThresholdSinks (we expect 1 now).
|
||||
res, ok = a.broker.SuccessThresholdSinks(eventlogger.EventType(event.AuditType.String()))
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 1, res)
|
||||
}
|
||||
|
||||
// TestAuditBroker_Deregister_SuccessThresholdSinks tests that we are able to
|
||||
// correctly identify what the required success threshold sinks value on the
|
||||
// eventlogger broker should be set to when deregistering audit backends.
|
||||
// We expect:
|
||||
// * 0 for only filtered backends
|
||||
// * 1 for any other combination
|
||||
func TestAuditBroker_Deregister_SuccessThresholdSinks(t *testing.T) {
|
||||
t.Parallel()
|
||||
l := corehelpers.NewTestLogger(t)
|
||||
a, err := NewAuditBroker(l)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, a)
|
||||
|
||||
filterBackend := testAuditBackend(t, "b1-filter", map[string]string{"filter": "operation == create"})
|
||||
noFilterBackend := testAuditBackend(t, "b2-no-filter", map[string]string{})
|
||||
|
||||
err = a.Register("b1-filter", filterBackend, false)
|
||||
require.NoError(t, err)
|
||||
err = a.Register("b2-no-filter", noFilterBackend, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
// We have a mix of filtered and non-filtered backends, so the
|
||||
// successThresholdSinks should be 1.
|
||||
res, ok := a.broker.SuccessThresholdSinks(eventlogger.EventType(event.AuditType.String()))
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 1, res)
|
||||
|
||||
// Deregister the non-filtered backend, there is one filtered backend left,
|
||||
// so the successThresholdSinks should be 0.
|
||||
err = a.Deregister(context.Background(), "b2-no-filter")
|
||||
require.NoError(t, err)
|
||||
res, ok = a.broker.SuccessThresholdSinks(eventlogger.EventType(event.AuditType.String()))
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 0, res)
|
||||
|
||||
// Deregister the last backend, disabling audit. The value of
|
||||
// successThresholdSinks should still be 0.
|
||||
err = a.Deregister(context.Background(), "b1-filter")
|
||||
require.NoError(t, err)
|
||||
res, ok = a.broker.SuccessThresholdSinks(eventlogger.EventType(event.AuditType.String()))
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 0, res)
|
||||
|
||||
// Re-register a backend that doesn't use filtering.
|
||||
err = a.Register("b2-no-filter", noFilterBackend, false)
|
||||
require.NoError(t, err)
|
||||
res, ok = a.broker.SuccessThresholdSinks(eventlogger.EventType(event.AuditType.String()))
|
||||
require.True(t, ok)
|
||||
require.Equal(t, 1, res)
|
||||
}
|
||||
|
||||
// TestAuditBroker_Register_Fallback ensures we can register a fallback device.
|
||||
func TestAuditBroker_Register_Fallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
l := corehelpers.NewTestLogger(t)
|
||||
a, err := NewAuditBroker(l)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, a)
|
||||
|
||||
path := "juan/"
|
||||
fallbackBackend := testAuditBackend(t, path, map[string]string{"fallback": "true"})
|
||||
err = a.Register(path, fallbackBackend, false)
|
||||
require.NoError(t, err)
|
||||
require.True(t, a.fallbackBroker.IsAnyPipelineRegistered(eventlogger.EventType(event.AuditType.String())))
|
||||
require.Equal(t, path, a.fallbackName)
|
||||
threshold, found := a.fallbackBroker.SuccessThresholdSinks(eventlogger.EventType(event.AuditType.String()))
|
||||
require.True(t, found)
|
||||
require.Equal(t, 1, threshold)
|
||||
}
|
||||
|
||||
// TestAuditBroker_Register_FallbackMultiple tests that trying to register more
|
||||
// than a single fallback device results in the correct error.
|
||||
func TestAuditBroker_Register_FallbackMultiple(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
l := corehelpers.NewTestLogger(t)
|
||||
a, err := NewAuditBroker(l)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, a)
|
||||
|
||||
path1 := "juan1/"
|
||||
fallbackBackend1 := testAuditBackend(t, path1, map[string]string{"fallback": "true"})
|
||||
err = a.Register(path1, fallbackBackend1, false)
|
||||
require.NoError(t, err)
|
||||
require.True(t, a.fallbackBroker.IsAnyPipelineRegistered(eventlogger.EventType(event.AuditType.String())))
|
||||
require.Equal(t, path1, a.fallbackName)
|
||||
|
||||
path2 := "juan2/"
|
||||
fallbackBackend2 := testAuditBackend(t, path2, map[string]string{"fallback": "true"})
|
||||
err = a.Register(path1, fallbackBackend2, false)
|
||||
require.Error(t, err)
|
||||
require.EqualError(t, err, "vault.(AuditBroker).Register: backend already registered 'juan1/'")
|
||||
require.True(t, a.fallbackBroker.IsAnyPipelineRegistered(eventlogger.EventType(event.AuditType.String())))
|
||||
require.Equal(t, path1, a.fallbackName)
|
||||
}
|
||||
|
||||
// TestAuditBroker_Deregister_Fallback ensures that we can deregister a fallback
|
||||
// device successfully.
|
||||
func TestAuditBroker_Deregister_Fallback(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
l := corehelpers.NewTestLogger(t)
|
||||
a, err := NewAuditBroker(l)
|
||||
require.NoError(t, err)
|
||||
require.NotNil(t, a)
|
||||
|
||||
path := "juan/"
|
||||
fallbackBackend := testAuditBackend(t, path, map[string]string{"fallback": "true"})
|
||||
err = a.Register(path, fallbackBackend, false)
|
||||
require.NoError(t, err)
|
||||
require.True(t, a.fallbackBroker.IsAnyPipelineRegistered(eventlogger.EventType(event.AuditType.String())))
|
||||
require.Equal(t, path, a.fallbackName)
|
||||
|
||||
threshold, found := a.fallbackBroker.SuccessThresholdSinks(eventlogger.EventType(event.AuditType.String()))
|
||||
require.True(t, found)
|
||||
require.Equal(t, 1, threshold)
|
||||
|
||||
err = a.Deregister(context.Background(), path)
|
||||
require.NoError(t, err)
|
||||
require.False(t, a.fallbackBroker.IsAnyPipelineRegistered(eventlogger.EventType(event.AuditType.String())))
|
||||
require.Equal(t, "", a.fallbackName)
|
||||
|
||||
threshold, found = a.fallbackBroker.SuccessThresholdSinks(eventlogger.EventType(event.AuditType.String()))
|
||||
require.True(t, found)
|
||||
require.Equal(t, 0, threshold)
|
||||
}
|
||||
|
||||
// TestAuditBroker_Deregister_Multiple ensures that we can call deregister multiple
|
||||
// times without issue if is no matching backend registered.
|
||||
func TestAuditBroker_Deregister_Multiple(t *testing.T) {
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
log "github.com/hashicorp/go-hclog"
|
||||
"github.com/hashicorp/go-uuid"
|
||||
"github.com/hashicorp/vault/audit"
|
||||
"github.com/hashicorp/vault/helper/constants"
|
||||
"github.com/hashicorp/vault/helper/namespace"
|
||||
"github.com/hashicorp/vault/helper/testhelpers/corehelpers"
|
||||
"github.com/hashicorp/vault/sdk/helper/jsonutil"
|
||||
@@ -235,69 +236,6 @@ func TestCore_EnableAudit_Local(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestAudit_enableAudit_fallback_invalid ensures that supplying a bad value for
|
||||
// 'fallback' in options gives us the correct error.
|
||||
func TestAudit_enableAudit_fallback_invalid(t *testing.T) {
|
||||
entry := &MountEntry{
|
||||
Path: "noop/",
|
||||
Options: map[string]string{
|
||||
"fallback": "juan",
|
||||
},
|
||||
}
|
||||
|
||||
cluster := NewTestCluster(t, nil, nil)
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
core := cluster.Cores[0]
|
||||
core.auditBackends["noop"] = corehelpers.NoopAuditFactory(nil)
|
||||
err := core.enableAudit(context.Background(), entry, false)
|
||||
require.Error(t, err)
|
||||
require.EqualError(t, err, "unable to enable audit device 'noop/', cannot parse supplied 'fallback' setting: cannot parse '' as bool: strconv.ParseBool: parsing \"juan\": invalid syntax")
|
||||
}
|
||||
|
||||
// TestAudit_enableAudit_fallback_two ensures trying to enable a second fallback
|
||||
// device returns the correct error.
|
||||
func TestAudit_enableAudit_fallback_two(t *testing.T) {
|
||||
entry1 := &MountEntry{
|
||||
Table: auditTableType,
|
||||
Path: "noop1/",
|
||||
Type: "noop",
|
||||
UUID: "abcd",
|
||||
Accessor: "noop1-abcd",
|
||||
NamespaceID: namespace.RootNamespaceID,
|
||||
Options: map[string]string{
|
||||
"fallback": "TRUE",
|
||||
},
|
||||
namespace: namespace.RootNamespace,
|
||||
}
|
||||
|
||||
entry2 := &MountEntry{
|
||||
Table: auditTableType,
|
||||
Path: "noop2/",
|
||||
Type: "noop",
|
||||
UUID: "abcd",
|
||||
Accessor: "noop2-abcd",
|
||||
NamespaceID: namespace.RootNamespaceID,
|
||||
Options: map[string]string{
|
||||
"fallback": "1",
|
||||
},
|
||||
namespace: namespace.RootNamespace,
|
||||
}
|
||||
|
||||
cluster := NewTestCluster(t, nil, nil)
|
||||
cluster.Start()
|
||||
defer cluster.Cleanup()
|
||||
core := cluster.Cores[0]
|
||||
core.auditBackends["noop"] = corehelpers.NoopAuditFactory(nil)
|
||||
ctx := namespace.ContextWithNamespace(context.Background(), namespace.RootNamespace)
|
||||
err := core.enableAudit(ctx, entry1, false)
|
||||
require.NoError(t, err)
|
||||
|
||||
err = core.enableAudit(ctx, entry2, false)
|
||||
require.Error(t, err)
|
||||
require.EqualError(t, err, "unable to enable audit device 'noop2/', a fallback device already exists 'noop1/'")
|
||||
}
|
||||
|
||||
func TestCore_DisableAudit(t *testing.T) {
|
||||
c, keys, _ := TestCoreUnsealed(t)
|
||||
c.auditBackends["noop"] = corehelpers.NoopAuditFactory(nil)
|
||||
@@ -685,3 +623,142 @@ func TestAuditBroker_AuditHeaders(t *testing.T) {
|
||||
t.Fatalf("err: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAudit_hasEnterpriseAuditOptions checks that the existence of any Enterprise
|
||||
// only options in the options which can be supplied to enable an audit device can
|
||||
// be flagged.
|
||||
func TestAudit_hasEnterpriseAuditOptions(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := map[string]struct {
|
||||
input map[string]string
|
||||
expected bool
|
||||
}{
|
||||
"nil": {
|
||||
expected: false,
|
||||
},
|
||||
"empty": {
|
||||
input: make(map[string]string),
|
||||
expected: false,
|
||||
},
|
||||
"non-ent-opts": {
|
||||
input: map[string]string{
|
||||
"log_raw": "true",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
"ent-opt-filter": {
|
||||
input: map[string]string{
|
||||
"filter": "mount_type == kv",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
"ent-opt-fallback": {
|
||||
input: map[string]string{
|
||||
"fallback": "true",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
"ent-opt-filter-and-fallback": {
|
||||
input: map[string]string{
|
||||
"filter": "mount_type == kv",
|
||||
"fallback": "true",
|
||||
},
|
||||
expected: true,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
name := name
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Equal(t, tc.expected, hasEnterpriseAuditOptions(tc.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAudit_hasInvalidAuditOptions tests that depending on whether we are running
|
||||
// an Enterprise or non-Enterprise version of Vault, the options supplied to enable
|
||||
// an audit device may or may not be valid.
|
||||
// NOTE: In the non-Enterprise version of Vault supplying audit options such as
|
||||
// 'filter' or 'fallback' is not allowed.
|
||||
func TestAudit_hasInvalidAuditOptions(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
input map[string]string
|
||||
expected bool
|
||||
}{
|
||||
"non-ent-opts": {
|
||||
input: map[string]string{
|
||||
"log_raw": "true",
|
||||
},
|
||||
expected: false,
|
||||
},
|
||||
"ent-opt": {
|
||||
input: map[string]string{
|
||||
"filter": "mount_type == kv",
|
||||
},
|
||||
expected: !constants.IsEnterprise,
|
||||
},
|
||||
}
|
||||
|
||||
for name, tc := range tests {
|
||||
name := name
|
||||
tc := tc
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
require.Equal(t, tc.expected, hasInvalidAuditOptions(tc.input))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAudit_enableAudit ensures that we do not enable an audit device with Enterprise
|
||||
// only options on a non-Enterprise version of Vault.
|
||||
func TestAudit_enableAudit(t *testing.T) {
|
||||
cluster := NewTestCluster(t, nil, &TestClusterOptions{NumCores: 1})
|
||||
defer cluster.Cleanup()
|
||||
|
||||
c := cluster.Cores[0]
|
||||
c.auditBackends["noop"] = corehelpers.NoopAuditFactory(nil)
|
||||
|
||||
me := &MountEntry{
|
||||
Table: auditTableType,
|
||||
Path: "foo",
|
||||
Type: "noop",
|
||||
Options: map[string]string{"fallback": "true"},
|
||||
}
|
||||
err := c.enableAudit(namespace.RootContext(context.Background()), me, true)
|
||||
|
||||
if constants.IsEnterprise {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAudit_newAuditBackend ensures that we do not create a new audit device
|
||||
// with Enterprise only options on a non-Enterprise version of Vault.
|
||||
func TestAudit_newAuditBackend(t *testing.T) {
|
||||
cluster := NewTestCluster(t, nil, &TestClusterOptions{NumCores: 1})
|
||||
defer cluster.Cleanup()
|
||||
|
||||
c := cluster.Cores[0]
|
||||
c.auditBackends["noop"] = corehelpers.NoopAuditFactory(nil)
|
||||
|
||||
me := &MountEntry{
|
||||
Table: auditTableType,
|
||||
Path: "foo",
|
||||
Type: "noop",
|
||||
Options: map[string]string{"fallback": "true"},
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
_, err := c.newAuditBackend(ctx, me, &logical.InmemStorage{}, me.Options)
|
||||
|
||||
if constants.IsEnterprise {
|
||||
require.NoError(t, err)
|
||||
} else {
|
||||
require.Error(t, err)
|
||||
}
|
||||
}
|
||||
|
||||
68
vault/external_tests/audit/audit_filtering_ce_test.go
Normal file
68
vault/external_tests/audit/audit_filtering_ce_test.go
Normal file
@@ -0,0 +1,68 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
//go:build !enterprise
|
||||
|
||||
package audit
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/helper/testhelpers/minimal"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestAuditFilteringInCE ensures that the audit device 'filter'
|
||||
// option is only supported in the enterprise edition of the product.
|
||||
func TestAuditFilteringInCE(t *testing.T) {
|
||||
t.Parallel()
|
||||
cluster := minimal.NewTestSoloCluster(t, nil)
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
// Attempt to create an audit device with filtering enabled.
|
||||
mountPointFilterDevicePath := "mountpoint"
|
||||
mountPointFilterDeviceData := map[string]any{
|
||||
"type": "file",
|
||||
"description": "",
|
||||
"local": false,
|
||||
"options": map[string]any{
|
||||
"file_path": "/tmp/audit.log",
|
||||
"filter": "mount_point == secret/",
|
||||
},
|
||||
}
|
||||
_, err := client.Logical().Write("sys/audit/"+mountPointFilterDevicePath, mountPointFilterDeviceData)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "enterprise-only options supplied")
|
||||
|
||||
// Ensure the device has not been created.
|
||||
devices, err := client.Sys().ListAudit()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, devices, 0)
|
||||
}
|
||||
|
||||
// TestAuditFilteringFallbackDeviceInCE validates that the audit device
|
||||
// 'fallback' option is only available in the enterprise edition of the product.
|
||||
func TestAuditFilteringFallbackDeviceInCE(t *testing.T) {
|
||||
t.Parallel()
|
||||
cluster := minimal.NewTestSoloCluster(t, nil)
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
fallbackDevicePath := "fallback"
|
||||
fallbackDeviceData := map[string]any{
|
||||
"type": "file",
|
||||
"description": "",
|
||||
"local": false,
|
||||
"options": map[string]any{
|
||||
"file_path": "/tmp/audit.log",
|
||||
"fallback": "true",
|
||||
},
|
||||
}
|
||||
_, err := client.Logical().Write("sys/audit/"+fallbackDevicePath, fallbackDeviceData)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "enterprise-only options supplied")
|
||||
|
||||
// Ensure the device has not been created.
|
||||
devices, err := client.Sys().ListAudit()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, devices, 0)
|
||||
}
|
||||
@@ -1,378 +0,0 @@
|
||||
// Copyright (c) HashiCorp, Inc.
|
||||
// SPDX-License-Identifier: BUSL-1.1
|
||||
|
||||
package audit
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/hashicorp/vault/helper/testhelpers/minimal"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
// TestAuditFilteringOnDifferentFields validates that the audit device 'filter'
|
||||
// option works as expected for the fields we allow filtering on. We create
|
||||
// three audit devices, each with a different filter, and make some auditable
|
||||
// requests, then we ensure that correct entries were written to the respective
|
||||
// log files. The mount_type and namespace filters are tested in other tests in
|
||||
// this package.
|
||||
func TestAuditFilteringOnDifferentFields(t *testing.T) {
|
||||
t.Parallel()
|
||||
cluster := minimal.NewTestSoloCluster(t, nil)
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
// Create audit devices.
|
||||
tempDir := t.TempDir()
|
||||
mountPointFilterLogFile, err := os.CreateTemp(tempDir, "")
|
||||
mountPointFilterDevicePath := "mountpoint"
|
||||
mountPointFilterDeviceData := map[string]any{
|
||||
"type": "file",
|
||||
"description": "",
|
||||
"local": false,
|
||||
"options": map[string]any{
|
||||
"file_path": mountPointFilterLogFile.Name(),
|
||||
"filter": "mount_point == secret/",
|
||||
},
|
||||
}
|
||||
_, err = client.Logical().Write("sys/audit/"+mountPointFilterDevicePath, mountPointFilterDeviceData)
|
||||
require.NoError(t, err)
|
||||
|
||||
operationFilterLogFile, err := os.CreateTemp(tempDir, "")
|
||||
operationFilterPath := "operation"
|
||||
operationFilterData := map[string]any{
|
||||
"type": "file",
|
||||
"description": "",
|
||||
"local": false,
|
||||
"options": map[string]any{
|
||||
"file_path": operationFilterLogFile.Name(),
|
||||
"filter": "operation == create",
|
||||
},
|
||||
}
|
||||
_, err = client.Logical().Write("sys/audit/"+operationFilterPath, operationFilterData)
|
||||
require.NoError(t, err)
|
||||
|
||||
pathFilterLogFile, err := os.CreateTemp(tempDir, "")
|
||||
pathFilterDevicePath := "path"
|
||||
pathFilterDeviceData := map[string]any{
|
||||
"type": "file",
|
||||
"description": "",
|
||||
"local": false,
|
||||
"options": map[string]any{
|
||||
"file_path": pathFilterLogFile.Name(),
|
||||
"filter": "path == secret/foo",
|
||||
},
|
||||
}
|
||||
_, err = client.Logical().Write("sys/audit/"+pathFilterDevicePath, pathFilterDeviceData)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Ensure the devices have been created.
|
||||
devices, err := client.Sys().ListAudit()
|
||||
require.NoError(t, err)
|
||||
_, ok := devices[mountPointFilterDevicePath+"/"]
|
||||
require.True(t, ok)
|
||||
_, ok = devices[operationFilterPath+"/"]
|
||||
require.True(t, ok)
|
||||
_, ok = devices[pathFilterDevicePath+"/"]
|
||||
require.True(t, ok)
|
||||
|
||||
// A write to KV should produce an audit entry that is written to all the
|
||||
// audit devices.
|
||||
data := map[string]any{
|
||||
"foo": "bar",
|
||||
}
|
||||
err = client.KVv1("secret/").Put(context.Background(), "foo", data)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Disable the audit devices.
|
||||
err = client.Sys().DisableAudit(mountPointFilterDevicePath)
|
||||
require.NoError(t, err)
|
||||
err = client.Sys().DisableAudit(operationFilterPath)
|
||||
require.NoError(t, err)
|
||||
err = client.Sys().DisableAudit(pathFilterDevicePath)
|
||||
require.NoError(t, err)
|
||||
// Ensure the devices are no longer there.
|
||||
devices, err = client.Sys().ListAudit()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, devices, 0)
|
||||
|
||||
// Validate that only the entries matching the filters were written to each log file.
|
||||
entries := checkAuditEntries(t, mountPointFilterLogFile, "mount_point", "secret/")
|
||||
require.Equal(t, 2, entries)
|
||||
entries = checkAuditEntries(t, operationFilterLogFile, "operation", "create")
|
||||
require.Equal(t, 2, entries)
|
||||
entries = checkAuditEntries(t, pathFilterLogFile, "path", "secret/foo")
|
||||
require.Equal(t, 2, entries)
|
||||
}
|
||||
|
||||
// TestAuditFilteringMultipleDevices validates that the audit device 'filter'
|
||||
// option works as expected and multiple audit devices with the same filter all
|
||||
// write the relevant entries to the logs. We create two audit devices that
|
||||
// filter out all events that are not for the KV mount type and one without
|
||||
// filters, make some auditable requests that both match and do not match the
|
||||
// filters, and ensure there are audit log entries for the former but not the
|
||||
// latter.
|
||||
func TestAuditFilteringMultipleDevices(t *testing.T) {
|
||||
t.Parallel()
|
||||
cluster := minimal.NewTestSoloCluster(t, nil)
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
// Create audit devices.
|
||||
tempDir := t.TempDir()
|
||||
filteredLogFile, err := os.CreateTemp(tempDir, "")
|
||||
filteredDevicePath := "filtered"
|
||||
filteredDeviceData := map[string]any{
|
||||
"type": "file",
|
||||
"description": "",
|
||||
"local": false,
|
||||
"options": map[string]any{
|
||||
"file_path": filteredLogFile.Name(),
|
||||
"filter": "mount_type == kv",
|
||||
},
|
||||
}
|
||||
_, err = client.Logical().Write("sys/audit/"+filteredDevicePath, filteredDeviceData)
|
||||
require.NoError(t, err)
|
||||
|
||||
filteredLogFile2, err := os.CreateTemp(tempDir, "")
|
||||
filteredDevicePath2 := "filtered2"
|
||||
filteredDeviceData2 := map[string]any{
|
||||
"type": "file",
|
||||
"description": "",
|
||||
"local": false,
|
||||
"options": map[string]any{
|
||||
"file_path": filteredLogFile2.Name(),
|
||||
"filter": "mount_type == kv",
|
||||
},
|
||||
}
|
||||
_, err = client.Logical().Write("sys/audit/"+filteredDevicePath2, filteredDeviceData2)
|
||||
require.NoError(t, err)
|
||||
|
||||
nonFilteredLogFile, err := os.CreateTemp(tempDir, "")
|
||||
nonFilteredDevicePath := "nonfiltered"
|
||||
nonFilteredDeviceData := map[string]any{
|
||||
"type": "file",
|
||||
"description": "",
|
||||
"local": false,
|
||||
"options": map[string]any{
|
||||
"file_path": nonFilteredLogFile.Name(),
|
||||
},
|
||||
}
|
||||
_, err = client.Logical().Write("sys/audit/"+nonFilteredDevicePath, nonFilteredDeviceData)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Ensure the devices have been created.
|
||||
devices, err := client.Sys().ListAudit()
|
||||
require.NoError(t, err)
|
||||
_, ok := devices[filteredDevicePath+"/"]
|
||||
require.True(t, ok)
|
||||
_, ok = devices[filteredDevicePath2+"/"]
|
||||
require.True(t, ok)
|
||||
_, ok = devices[nonFilteredDevicePath+"/"]
|
||||
require.True(t, ok)
|
||||
|
||||
// Ensure the non-filtered log file is not empty.
|
||||
nonFilteredLogSize := getFileSize(t, nonFilteredLogFile.Name())
|
||||
require.Positive(t, nonFilteredLogSize)
|
||||
|
||||
// A write to KV should produce an audit entry that is written to the
|
||||
// filtered devices and the non-filtered device.
|
||||
data := map[string]any{
|
||||
"foo": "bar",
|
||||
}
|
||||
err = client.KVv1("secret/").Put(context.Background(), "foo", data)
|
||||
require.NoError(t, err)
|
||||
// Ensure the non-filtered log file was written to.
|
||||
oldNonFilteredLogSize := nonFilteredLogSize
|
||||
nonFilteredLogSize = getFileSize(t, nonFilteredLogFile.Name())
|
||||
require.Greater(t, nonFilteredLogSize, oldNonFilteredLogSize)
|
||||
|
||||
// Parse the filtered logs and verify that the filters only allowed entries
|
||||
// with mount_type value being 'kv'. While we're at it, ensure that the
|
||||
// numbers of entries are correct in both files.
|
||||
filteredLogFiles := []*os.File{filteredLogFile, filteredLogFile2}
|
||||
for _, f := range filteredLogFiles {
|
||||
numberOfEntries := checkAuditEntries(t, f, "mount_type", "kv")
|
||||
require.Equal(t, 2, numberOfEntries)
|
||||
}
|
||||
|
||||
// Disable the audit devices.
|
||||
err = client.Sys().DisableAudit(filteredDevicePath)
|
||||
require.NoError(t, err)
|
||||
err = client.Sys().DisableAudit(filteredDevicePath2)
|
||||
require.NoError(t, err)
|
||||
err = client.Sys().DisableAudit(nonFilteredDevicePath)
|
||||
require.NoError(t, err)
|
||||
// Ensure the devices are no longer there.
|
||||
devices, err = client.Sys().ListAudit()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, devices, 0)
|
||||
}
|
||||
|
||||
// TestAuditFilteringFallbackDevice validates that the audit device 'fallback'
|
||||
// option works as expected. We create two audit devices, one with 'fallback'
|
||||
// enabled and one with a filter, and make some auditable requests, then we
|
||||
// ensure that correct entries were written to the respective log files.
|
||||
func TestAuditFilteringFallbackDevice(t *testing.T) {
|
||||
t.Parallel()
|
||||
cluster := minimal.NewTestSoloCluster(t, nil)
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
tempDir := t.TempDir()
|
||||
fallbackLogFile, err := os.CreateTemp(tempDir, "")
|
||||
fallbackDevicePath := "fallback"
|
||||
fallbackDeviceData := map[string]any{
|
||||
"type": "file",
|
||||
"description": "",
|
||||
"local": false,
|
||||
"options": map[string]any{
|
||||
"file_path": fallbackLogFile.Name(),
|
||||
"fallback": "true",
|
||||
},
|
||||
}
|
||||
_, err = client.Logical().Write("sys/audit/"+fallbackDevicePath, fallbackDeviceData)
|
||||
require.NoError(t, err)
|
||||
|
||||
filteredLogFile, err := os.CreateTemp(tempDir, "")
|
||||
filteredDevicePath := "filtered"
|
||||
filteredDeviceData := map[string]any{
|
||||
"type": "file",
|
||||
"description": "",
|
||||
"local": false,
|
||||
"options": map[string]any{
|
||||
"file_path": filteredLogFile.Name(),
|
||||
"filter": "mount_type == kv",
|
||||
},
|
||||
}
|
||||
_, err = client.Logical().Write("sys/audit/"+filteredDevicePath, filteredDeviceData)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Ensure the devices have been created.
|
||||
devices, err := client.Sys().ListAudit()
|
||||
require.NoError(t, err)
|
||||
_, ok := devices[fallbackDevicePath+"/"]
|
||||
require.True(t, ok)
|
||||
_, ok = devices[filteredDevicePath+"/"]
|
||||
require.True(t, ok)
|
||||
|
||||
// A write to KV should produce an audit entry that is written to the
|
||||
// filtered device.
|
||||
data := map[string]any{
|
||||
"foo": "bar",
|
||||
}
|
||||
err = client.KVv1("secret/").Put(context.Background(), "foo", data)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Disable the audit devices.
|
||||
err = client.Sys().DisableAudit(fallbackDevicePath)
|
||||
require.NoError(t, err)
|
||||
err = client.Sys().DisableAudit(filteredDevicePath)
|
||||
require.NoError(t, err)
|
||||
// Ensure the devices are no longer there.
|
||||
devices, err = client.Sys().ListAudit()
|
||||
require.Len(t, devices, 0)
|
||||
|
||||
// Validate that only the entries matching the filter were written to the filtered log file.
|
||||
numberOfEntries := checkAuditEntries(t, filteredLogFile, "mount_type", "kv")
|
||||
require.Equal(t, 2, numberOfEntries)
|
||||
|
||||
// Validate that only the entries NOT matching the filter were written to the fallback log file.
|
||||
numberOfEntries = 0
|
||||
scanner := bufio.NewScanner(fallbackLogFile)
|
||||
var auditRecord map[string]any
|
||||
for scanner.Scan() {
|
||||
auditRequest := map[string]any{}
|
||||
err := json.Unmarshal(scanner.Bytes(), &auditRecord)
|
||||
require.NoError(t, err)
|
||||
req, ok := auditRecord["request"]
|
||||
require.True(t, ok, "failed to parse request data from audit log entry")
|
||||
|
||||
auditRequest = req.(map[string]any)
|
||||
require.NotEqual(t, "kv", auditRequest["mount_type"])
|
||||
numberOfEntries += 1
|
||||
}
|
||||
// the fallback device will catch all non-kv related entries such as login, etc. there should be 7 in total.
|
||||
require.Equal(t, 7, numberOfEntries)
|
||||
}
|
||||
|
||||
// TestAuditFilteringFilterForUnsupportedField validates that the audit device
|
||||
// 'filter' option fails when the filter expression selector references an
|
||||
// unsupported field and that the error prevents an audit device from being
|
||||
// created.
|
||||
func TestAuditFilteringFilterForUnsupportedField(t *testing.T) {
|
||||
t.Parallel()
|
||||
cluster := minimal.NewTestSoloCluster(t, nil)
|
||||
client := cluster.Cores[0].Client
|
||||
|
||||
tempDir := t.TempDir()
|
||||
filteredLogFile, err := os.CreateTemp(tempDir, "")
|
||||
filteredDevicePath := "filtered"
|
||||
filteredDeviceData := map[string]any{
|
||||
"type": "file",
|
||||
"description": "",
|
||||
"local": false,
|
||||
"options": map[string]any{
|
||||
"file_path": filteredLogFile.Name(),
|
||||
"filter": "auth == foo", // 'auth' is not one of the fields we allow filtering on
|
||||
},
|
||||
}
|
||||
_, err = client.Logical().Write("sys/audit/"+filteredDevicePath, filteredDeviceData)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "audit.NewEntryFilter: filter references an unsupported field: auth == foo")
|
||||
|
||||
// Ensure the device has not been created.
|
||||
devices, err := client.Sys().ListAudit()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, devices, 0)
|
||||
|
||||
// Now we do the same test but with the 'skip_test' option set to true.
|
||||
filteredDeviceDataSkipTest := map[string]any{
|
||||
"type": "file",
|
||||
"description": "",
|
||||
"local": false,
|
||||
"options": map[string]any{
|
||||
"file_path": filteredLogFile.Name(),
|
||||
"filter": "auth == foo", // 'auth' is not one of the fields we allow filtering on
|
||||
"skip_test": true,
|
||||
},
|
||||
}
|
||||
_, err = client.Logical().Write("sys/audit/"+filteredDevicePath, filteredDeviceDataSkipTest)
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, "audit.NewEntryFilter: filter references an unsupported field: auth == foo")
|
||||
|
||||
// Ensure the device has not been created.
|
||||
devices, err = client.Sys().ListAudit()
|
||||
require.NoError(t, err)
|
||||
require.Len(t, devices, 0)
|
||||
}
|
||||
|
||||
// getFileSize returns the size of the given file in bytes.
|
||||
func getFileSize(t *testing.T, filePath string) int64 {
|
||||
t.Helper()
|
||||
fi, err := os.Stat(filePath)
|
||||
require.NoError(t, err)
|
||||
return fi.Size()
|
||||
}
|
||||
|
||||
// checkAuditEntries parses the audit log file and asserts that the given key
|
||||
// has the expected value for each entry. It returns the number of entries that
|
||||
// were parsed.
|
||||
func checkAuditEntries(t *testing.T, logFile *os.File, key string, expectedValue any) int {
|
||||
t.Helper()
|
||||
counter := 0
|
||||
scanner := bufio.NewScanner(logFile)
|
||||
var auditRecord map[string]any
|
||||
for scanner.Scan() {
|
||||
auditRequest := map[string]any{}
|
||||
err := json.Unmarshal(scanner.Bytes(), &auditRecord)
|
||||
require.NoError(t, err)
|
||||
req, ok := auditRecord["request"]
|
||||
require.True(t, ok, "failed to parse request data from audit log entry")
|
||||
auditRequest = req.(map[string]any)
|
||||
require.Equal(t, expectedValue, auditRequest[key])
|
||||
counter += 1
|
||||
}
|
||||
return counter
|
||||
}
|
||||
Reference in New Issue
Block a user