mirror of
https://github.com/hashicorp/vault.git
synced 2025-08-22 07:01:09 +02:00
* audit: entry_formatter update to ensure no race detection issues * in progress with looking at a clone method for LogInput * Tidy up LogInput Clone method * less memory allocation * fix hmac key clone
401 lines
12 KiB
Go
401 lines
12 KiB
Go
// Copyright (c) HashiCorp, Inc.
|
|
// SPDX-License-Identifier: BUSL-1.1
|
|
|
|
package audit
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/hashicorp/vault/internal/observability/event"
|
|
|
|
"github.com/hashicorp/vault/helper/namespace"
|
|
|
|
"github.com/hashicorp/vault/sdk/logical"
|
|
|
|
"github.com/hashicorp/eventlogger"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// fakeEvent will return a new fake event containing audit data based on the
|
|
// specified subtype, format and logical.LogInput.
|
|
func fakeEvent(tb testing.TB, subtype subtype, format format, input *logical.LogInput) *eventlogger.Event {
|
|
tb.Helper()
|
|
|
|
date := time.Date(2023, time.July, 11, 15, 49, 10, 0o0, time.Local)
|
|
|
|
auditEvent, err := NewEvent(subtype,
|
|
WithID("123"),
|
|
WithNow(date),
|
|
)
|
|
require.NoError(tb, err)
|
|
require.NotNil(tb, auditEvent)
|
|
require.Equal(tb, "123", auditEvent.ID)
|
|
require.Equal(tb, "v0.1", auditEvent.Version)
|
|
require.Equal(tb, subtype, auditEvent.Subtype)
|
|
require.Equal(tb, date, auditEvent.Timestamp)
|
|
|
|
auditEvent.Data = input
|
|
|
|
e := &eventlogger.Event{
|
|
Type: eventlogger.EventType(event.AuditType),
|
|
CreatedAt: auditEvent.Timestamp,
|
|
Formatted: make(map[string][]byte),
|
|
Payload: auditEvent,
|
|
}
|
|
|
|
return e
|
|
}
|
|
|
|
// TestNewEntryFormatter ensures we can create new EntryFormatter structs.
|
|
func TestNewEntryFormatter(t *testing.T) {
|
|
tests := map[string]struct {
|
|
UseStaticSalt bool
|
|
Options []Option // Only supports WithPrefix
|
|
IsErrorExpected bool
|
|
ExpectedErrorMessage string
|
|
ExpectedFormat format
|
|
ExpectedPrefix string
|
|
}{
|
|
"nil-salter": {
|
|
UseStaticSalt: false,
|
|
IsErrorExpected: true,
|
|
ExpectedErrorMessage: "audit.NewEntryFormatter: cannot create a new audit formatter with nil salter: invalid parameter",
|
|
},
|
|
"static-salter": {
|
|
UseStaticSalt: true,
|
|
IsErrorExpected: false,
|
|
Options: []Option{
|
|
WithFormat(JSONFormat.String()),
|
|
},
|
|
ExpectedFormat: JSONFormat,
|
|
},
|
|
"default": {
|
|
UseStaticSalt: true,
|
|
IsErrorExpected: false,
|
|
ExpectedFormat: JSONFormat,
|
|
},
|
|
"config-json": {
|
|
UseStaticSalt: true,
|
|
Options: []Option{
|
|
WithFormat(JSONFormat.String()),
|
|
},
|
|
IsErrorExpected: false,
|
|
ExpectedFormat: JSONFormat,
|
|
},
|
|
"config-jsonx": {
|
|
UseStaticSalt: true,
|
|
Options: []Option{
|
|
WithFormat(JSONxFormat.String()),
|
|
},
|
|
IsErrorExpected: false,
|
|
ExpectedFormat: JSONxFormat,
|
|
},
|
|
"config-json-prefix": {
|
|
UseStaticSalt: true,
|
|
Options: []Option{
|
|
WithPrefix("foo"),
|
|
WithFormat(JSONFormat.String()),
|
|
},
|
|
IsErrorExpected: false,
|
|
ExpectedFormat: JSONFormat,
|
|
ExpectedPrefix: "foo",
|
|
},
|
|
"config-jsonx-prefix": {
|
|
UseStaticSalt: true,
|
|
Options: []Option{
|
|
WithPrefix("foo"),
|
|
WithFormat(JSONxFormat.String()),
|
|
},
|
|
IsErrorExpected: false,
|
|
ExpectedFormat: JSONxFormat,
|
|
ExpectedPrefix: "foo",
|
|
},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
name := name
|
|
tc := tc
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
var ss Salter
|
|
if tc.UseStaticSalt {
|
|
ss = newStaticSalt(t)
|
|
}
|
|
|
|
cfg, err := NewFormatterConfig(tc.Options...)
|
|
require.NoError(t, err)
|
|
f, err := NewEntryFormatter(cfg, ss, tc.Options...)
|
|
|
|
switch {
|
|
case tc.IsErrorExpected:
|
|
require.Error(t, err)
|
|
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
|
require.Nil(t, f)
|
|
default:
|
|
require.NoError(t, err)
|
|
require.NotNil(t, f)
|
|
require.Equal(t, tc.ExpectedFormat, f.config.RequiredFormat)
|
|
require.Equal(t, tc.ExpectedPrefix, f.prefix)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestEntryFormatter_Reopen ensures that we do not get an error when calling Reopen.
|
|
func TestEntryFormatter_Reopen(t *testing.T) {
|
|
ss := newStaticSalt(t)
|
|
cfg, err := NewFormatterConfig()
|
|
require.NoError(t, err)
|
|
|
|
f, err := NewEntryFormatter(cfg, ss)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, f)
|
|
require.NoError(t, f.Reopen())
|
|
}
|
|
|
|
// TestEntryFormatter_Type ensures that the node is a 'formatter' type.
|
|
func TestEntryFormatter_Type(t *testing.T) {
|
|
ss := newStaticSalt(t)
|
|
cfg, err := NewFormatterConfig()
|
|
require.NoError(t, err)
|
|
|
|
f, err := NewEntryFormatter(cfg, ss)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, f)
|
|
require.Equal(t, eventlogger.NodeTypeFormatter, f.Type())
|
|
}
|
|
|
|
// TestEntryFormatter_Process attempts to run the Process method to convert the
|
|
// logical.LogInput within an audit event to JSON and JSONx (RequestEntry or ResponseEntry).
|
|
func TestEntryFormatter_Process(t *testing.T) {
|
|
tests := map[string]struct {
|
|
IsErrorExpected bool
|
|
ExpectedErrorMessage string
|
|
Subtype subtype
|
|
RequiredFormat format
|
|
Data *logical.LogInput
|
|
RootNamespace bool
|
|
}{
|
|
"json-request-no-data": {
|
|
IsErrorExpected: true,
|
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: cannot audit event (AuditRequest) with no data: invalid parameter",
|
|
Subtype: RequestType,
|
|
RequiredFormat: JSONFormat,
|
|
Data: nil,
|
|
},
|
|
"json-response-no-data": {
|
|
IsErrorExpected: true,
|
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: cannot audit event (AuditResponse) with no data: invalid parameter",
|
|
Subtype: ResponseType,
|
|
RequiredFormat: JSONFormat,
|
|
Data: nil,
|
|
},
|
|
"json-request-basic-input": {
|
|
IsErrorExpected: true,
|
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse request from audit event: request to request-audit a nil request",
|
|
Subtype: RequestType,
|
|
RequiredFormat: JSONFormat,
|
|
Data: &logical.LogInput{Type: "magic"},
|
|
},
|
|
"json-response-basic-input": {
|
|
IsErrorExpected: true,
|
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse response from audit event: request to response-audit a nil request",
|
|
Subtype: ResponseType,
|
|
RequiredFormat: JSONFormat,
|
|
Data: &logical.LogInput{Type: "magic"},
|
|
},
|
|
"json-request-basic-input-and-request-no-ns": {
|
|
IsErrorExpected: true,
|
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse request from audit event: no namespace",
|
|
Subtype: RequestType,
|
|
RequiredFormat: JSONFormat,
|
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
|
},
|
|
"json-response-basic-input-and-request-no-ns": {
|
|
IsErrorExpected: true,
|
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse response from audit event: no namespace",
|
|
Subtype: ResponseType,
|
|
RequiredFormat: JSONFormat,
|
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
|
},
|
|
"json-request-basic-input-and-request-with-ns": {
|
|
IsErrorExpected: false,
|
|
Subtype: RequestType,
|
|
RequiredFormat: JSONFormat,
|
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
|
RootNamespace: true,
|
|
},
|
|
"json-response-basic-input-and-request-with-ns": {
|
|
IsErrorExpected: false,
|
|
Subtype: ResponseType,
|
|
RequiredFormat: JSONFormat,
|
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
|
RootNamespace: true,
|
|
},
|
|
"jsonx-request-no-data": {
|
|
IsErrorExpected: true,
|
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: cannot audit event (AuditRequest) with no data: invalid parameter",
|
|
Subtype: RequestType,
|
|
RequiredFormat: JSONxFormat,
|
|
Data: nil,
|
|
},
|
|
"jsonx-response-no-data": {
|
|
IsErrorExpected: true,
|
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: cannot audit event (AuditResponse) with no data: invalid parameter",
|
|
Subtype: ResponseType,
|
|
RequiredFormat: JSONxFormat,
|
|
Data: nil,
|
|
},
|
|
"jsonx-request-basic-input": {
|
|
IsErrorExpected: true,
|
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse request from audit event: request to request-audit a nil request",
|
|
Subtype: RequestType,
|
|
RequiredFormat: JSONxFormat,
|
|
Data: &logical.LogInput{Type: "magic"},
|
|
},
|
|
"jsonx-response-basic-input": {
|
|
IsErrorExpected: true,
|
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse response from audit event: request to response-audit a nil request",
|
|
Subtype: ResponseType,
|
|
RequiredFormat: JSONxFormat,
|
|
Data: &logical.LogInput{Type: "magic"},
|
|
},
|
|
"jsonx-request-basic-input-and-request-no-ns": {
|
|
IsErrorExpected: true,
|
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse request from audit event: no namespace",
|
|
Subtype: RequestType,
|
|
RequiredFormat: JSONxFormat,
|
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
|
},
|
|
"jsonx-response-basic-input-and-request-no-ns": {
|
|
IsErrorExpected: true,
|
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse response from audit event: no namespace",
|
|
Subtype: ResponseType,
|
|
RequiredFormat: JSONxFormat,
|
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
|
},
|
|
"jsonx-request-basic-input-and-request-with-ns": {
|
|
IsErrorExpected: false,
|
|
Subtype: RequestType,
|
|
RequiredFormat: JSONxFormat,
|
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
|
RootNamespace: true,
|
|
},
|
|
"jsonx-response-basic-input-and-request-with-ns": {
|
|
IsErrorExpected: false,
|
|
Subtype: ResponseType,
|
|
RequiredFormat: JSONxFormat,
|
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
|
RootNamespace: true,
|
|
},
|
|
}
|
|
|
|
for name, tc := range tests {
|
|
name := name
|
|
tc := tc
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
e := fakeEvent(t, tc.Subtype, tc.RequiredFormat, tc.Data)
|
|
require.NotNil(t, e)
|
|
|
|
ss := newStaticSalt(t)
|
|
cfg, err := NewFormatterConfig(WithFormat(tc.RequiredFormat.String()))
|
|
require.NoError(t, err)
|
|
|
|
f, err := NewEntryFormatter(cfg, ss)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, f)
|
|
|
|
var ctx context.Context
|
|
switch {
|
|
case tc.RootNamespace:
|
|
ctx = namespace.RootContext(context.Background())
|
|
default:
|
|
ctx = context.Background()
|
|
}
|
|
|
|
processed, err := f.Process(ctx, e)
|
|
b, found := e.Format(string(tc.RequiredFormat))
|
|
|
|
switch {
|
|
case tc.IsErrorExpected:
|
|
require.Error(t, err)
|
|
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
|
require.Nil(t, processed)
|
|
require.False(t, found)
|
|
require.Nil(t, b)
|
|
default:
|
|
require.NoError(t, err)
|
|
require.NotNil(t, processed)
|
|
require.True(t, found)
|
|
require.NotNil(t, b)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// BenchmarkAuditFileSink_Process benchmarks the EntryFormatter and then event.FileSink calling Process.
|
|
// This should replicate the original benchmark testing which used to perform both of these roles together.
|
|
func BenchmarkAuditFileSink_Process(b *testing.B) {
|
|
// Base input
|
|
in := &logical.LogInput{
|
|
Auth: &logical.Auth{
|
|
ClientToken: "foo",
|
|
Accessor: "bar",
|
|
EntityID: "foobarentity",
|
|
DisplayName: "testtoken",
|
|
NoDefaultPolicy: true,
|
|
Policies: []string{"root"},
|
|
TokenType: logical.TokenTypeService,
|
|
},
|
|
Request: &logical.Request{
|
|
Operation: logical.UpdateOperation,
|
|
Path: "/foo",
|
|
Connection: &logical.Connection{
|
|
RemoteAddr: "127.0.0.1",
|
|
},
|
|
WrapInfo: &logical.RequestWrapInfo{
|
|
TTL: 60 * time.Second,
|
|
},
|
|
Headers: map[string][]string{
|
|
"foo": {"bar"},
|
|
},
|
|
},
|
|
}
|
|
|
|
ctx := namespace.RootContext(context.Background())
|
|
|
|
// Create the formatter node.
|
|
cfg, err := NewFormatterConfig()
|
|
require.NoError(b, err)
|
|
ss := newStaticSalt(b)
|
|
formatter, err := NewEntryFormatter(cfg, ss)
|
|
require.NoError(b, err)
|
|
require.NotNil(b, formatter)
|
|
|
|
// Create the sink node.
|
|
sink, err := event.NewFileSink("/dev/null", JSONFormat.String())
|
|
require.NoError(b, err)
|
|
require.NotNil(b, sink)
|
|
|
|
// Generate the event
|
|
event := fakeEvent(b, RequestType, JSONFormat, in)
|
|
require.NotNil(b, event)
|
|
|
|
b.ResetTimer()
|
|
b.RunParallel(func(pb *testing.PB) {
|
|
for pb.Next() {
|
|
event, err = formatter.Process(ctx, event)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
_, err := sink.Process(ctx, event)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
}
|
|
})
|
|
}
|