diff --git a/builtin/audit/file/backend.go b/builtin/audit/file/backend.go index 795aa8c300..86d0a96eb1 100644 --- a/builtin/audit/file/backend.go +++ b/builtin/audit/file/backend.go @@ -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" diff --git a/builtin/audit/file/backend_filter_node.go b/builtin/audit/file/backend_filter_node.go new file mode 100644 index 0000000000..6ab19bd9a3 --- /dev/null +++ b/builtin/audit/file/backend_filter_node.go @@ -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 +} diff --git a/builtin/audit/file/backend_filter_node_test.go b/builtin/audit/file/backend_filter_node_test.go new file mode 100644 index 0000000000..3c821393ee --- /dev/null +++ b/builtin/audit/file/backend_filter_node_test.go @@ -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()) +} diff --git a/builtin/audit/file/backend_test.go b/builtin/audit/file/backend_test.go index f3e495b343..3f1e118c98 100644 --- a/builtin/audit/file/backend_test.go +++ b/builtin/audit/file/backend_test.go @@ -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) { diff --git a/builtin/audit/socket/backend.go b/builtin/audit/socket/backend.go index c88c922a10..d9f96b2d76 100644 --- a/builtin/audit/socket/backend.go +++ b/builtin/audit/socket/backend.go @@ -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" diff --git a/builtin/audit/socket/backend_filter_node.go b/builtin/audit/socket/backend_filter_node.go new file mode 100644 index 0000000000..6d6f81e15b --- /dev/null +++ b/builtin/audit/socket/backend_filter_node.go @@ -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 +} diff --git a/builtin/audit/socket/backend_filter_node_test.go b/builtin/audit/socket/backend_filter_node_test.go new file mode 100644 index 0000000000..d8c26c2cc1 --- /dev/null +++ b/builtin/audit/socket/backend_filter_node_test.go @@ -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()) +} diff --git a/builtin/audit/socket/backend_test.go b/builtin/audit/socket/backend_test.go index 1c94e5ce6a..2a2dcee2bf 100644 --- a/builtin/audit/socket/backend_test.go +++ b/builtin/audit/socket/backend_test.go @@ -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) { diff --git a/builtin/audit/syslog/backend.go b/builtin/audit/syslog/backend.go index 28d2d6f59b..41a6607b68 100644 --- a/builtin/audit/syslog/backend.go +++ b/builtin/audit/syslog/backend.go @@ -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" diff --git a/builtin/audit/syslog/backend_filter_node.go b/builtin/audit/syslog/backend_filter_node.go new file mode 100644 index 0000000000..45798d48e6 --- /dev/null +++ b/builtin/audit/syslog/backend_filter_node.go @@ -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 +} diff --git a/builtin/audit/syslog/backend_filter_node_test.go b/builtin/audit/syslog/backend_filter_node_test.go new file mode 100644 index 0000000000..402212dbbf --- /dev/null +++ b/builtin/audit/syslog/backend_filter_node_test.go @@ -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()) +} diff --git a/builtin/audit/syslog/backend_test.go b/builtin/audit/syslog/backend_test.go index fb1e297b32..72765e3d6b 100644 --- a/builtin/audit/syslog/backend_test.go +++ b/builtin/audit/syslog/backend_test.go @@ -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) { diff --git a/vault/audit.go b/vault/audit.go index 0f0182be47..f4429c5921 100644 --- a/vault/audit.go +++ b/vault/audit.go @@ -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 +} diff --git a/vault/audit_broker_test.go b/vault/audit_broker_test.go index 6493f5454b..f032f8b11c 100644 --- a/vault/audit_broker_test.go +++ b/vault/audit_broker_test.go @@ -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) { diff --git a/vault/audit_test.go b/vault/audit_test.go index e58764cb24..cd25a1e1c6 100644 --- a/vault/audit_test.go +++ b/vault/audit_test.go @@ -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) + } +} diff --git a/vault/external_tests/audit/audit_filtering_ce_test.go b/vault/external_tests/audit/audit_filtering_ce_test.go new file mode 100644 index 0000000000..4f1a54f24f --- /dev/null +++ b/vault/external_tests/audit/audit_filtering_ce_test.go @@ -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) +} diff --git a/vault/external_tests/audit/audit_filtering_test.go b/vault/external_tests/audit/audit_filtering_test.go deleted file mode 100644 index 36750166c0..0000000000 --- a/vault/external_tests/audit/audit_filtering_test.go +++ /dev/null @@ -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 -}