Audit: Entry formatting is the only supported way to do audit (#24867)

* removed 'writer' related code as we only do formatting within the eventlogger

* re-added ported test elide list responses
This commit is contained in:
Peter Wilson
2024-01-15 21:04:21 +00:00
committed by GitHub
parent ecb50a4cb3
commit ff0d1ff4c9
8 changed files with 673 additions and 989 deletions

View File

@@ -16,6 +16,7 @@ import (
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/internal/observability/event"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/salt"
"github.com/hashicorp/vault/sdk/logical"
"github.com/jefferai/jsonx"
)
@@ -593,3 +594,8 @@ func newTemporaryEntryFormatter(n *EntryFormatter) *EntryFormatter {
prefix: n.prefix,
}
}
// Salt returns a new salt with default configuration and no storage usage, and no error.
func (s *nonPersistentSalt) Salt(_ context.Context) (*salt.Salt, error) {
return salt.NewNonpersistentSalt(), nil
}

View File

@@ -5,48 +5,57 @@ package audit
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"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/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/internal/observability/event"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/helper/salt"
"github.com/hashicorp/vault/sdk/logical"
"github.com/mitchellh/copystructure"
"github.com/stretchr/testify/assert"
"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
const testFormatJSONReqBasicStrFmt = `
{
"time": "2015-08-05T13:45:46Z",
"type": "request",
"auth": {
"client_token": "%s",
"accessor": "bar",
"display_name": "testtoken",
"policies": [
"root"
],
"no_default_policy": true,
"metadata": null,
"entity_id": "foobarentity",
"token_type": "service",
"token_ttl": 14400,
"token_issue_time": "2020-05-28T13:40:18-05:00"
},
"request": {
"operation": "update",
"path": "/foo",
"data": null,
"wrap_ttl": 60,
"remote_address": "127.0.0.1",
"headers": {
"foo": [
"bar"
]
}
},
"error": "this is an error"
}
`
// TestNewEntryFormatter ensures we can create new EntryFormatter structs.
func TestNewEntryFormatter(t *testing.T) {
@@ -381,20 +390,640 @@ func BenchmarkAuditFileSink_Process(b *testing.B) {
require.NotNil(b, sink)
// Generate the event
event := fakeEvent(b, RequestType, JSONFormat, in)
require.NotNil(b, event)
e := fakeEvent(b, RequestType, JSONFormat, in)
require.NotNil(b, e)
b.ResetTimer()
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
event, err = formatter.Process(ctx, event)
e, err = formatter.Process(ctx, e)
if err != nil {
panic(err)
}
_, err := sink.Process(ctx, event)
_, err := sink.Process(ctx, e)
if err != nil {
panic(err)
}
}
})
}
// TestEntryFormatter_FormatRequest exercises EntryFormatter.FormatRequest with
// varying inputs.
func TestEntryFormatter_FormatRequest(t *testing.T) {
tests := map[string]struct {
Input *logical.LogInput
IsErrorExpected bool
ExpectedErrorMessage string
RootNamespace bool
}{
"nil": {
Input: nil,
IsErrorExpected: true,
ExpectedErrorMessage: "request to request-audit a nil request",
},
"basic-input": {
Input: &logical.LogInput{},
IsErrorExpected: true,
ExpectedErrorMessage: "request to request-audit a nil request",
},
"input-and-request-no-ns": {
Input: &logical.LogInput{Request: &logical.Request{ID: "123"}},
IsErrorExpected: true,
ExpectedErrorMessage: "no namespace",
RootNamespace: false,
},
"input-and-request-with-ns": {
Input: &logical.LogInput{Request: &logical.Request{ID: "123"}},
IsErrorExpected: false,
RootNamespace: true,
},
}
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
ss := newStaticSalt(t)
cfg, err := NewFormatterConfig()
require.NoError(t, err)
f, err := NewEntryFormatter(cfg, ss)
require.NoError(t, err)
var ctx context.Context
switch {
case tc.RootNamespace:
ctx = namespace.RootContext(context.Background())
default:
ctx = context.Background()
}
entry, err := f.FormatRequest(ctx, tc.Input)
switch {
case tc.IsErrorExpected:
require.Error(t, err)
require.EqualError(t, err, tc.ExpectedErrorMessage)
require.Nil(t, entry)
default:
require.NoError(t, err)
require.NotNil(t, entry)
}
})
}
}
// TestEntryFormatter_FormatResponse exercises EntryFormatter.FormatResponse with
// varying inputs.
func TestEntryFormatter_FormatResponse(t *testing.T) {
tests := map[string]struct {
Input *logical.LogInput
IsErrorExpected bool
ExpectedErrorMessage string
RootNamespace bool
}{
"nil": {
Input: nil,
IsErrorExpected: true,
ExpectedErrorMessage: "request to response-audit a nil request",
},
"basic-input": {
Input: &logical.LogInput{},
IsErrorExpected: true,
ExpectedErrorMessage: "request to response-audit a nil request",
},
"input-and-request-no-ns": {
Input: &logical.LogInput{Request: &logical.Request{ID: "123"}},
IsErrorExpected: true,
ExpectedErrorMessage: "no namespace",
RootNamespace: false,
},
"input-and-request-with-ns": {
Input: &logical.LogInput{Request: &logical.Request{ID: "123"}},
IsErrorExpected: false,
RootNamespace: true,
},
}
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
ss := newStaticSalt(t)
cfg, err := NewFormatterConfig()
require.NoError(t, err)
f, err := NewEntryFormatter(cfg, ss)
require.NoError(t, err)
var ctx context.Context
switch {
case tc.RootNamespace:
ctx = namespace.RootContext(context.Background())
default:
ctx = context.Background()
}
entry, err := f.FormatResponse(ctx, tc.Input)
switch {
case tc.IsErrorExpected:
require.Error(t, err)
require.EqualError(t, err, tc.ExpectedErrorMessage)
require.Nil(t, entry)
default:
require.NoError(t, err)
require.NotNil(t, entry)
}
})
}
}
// TestEntryFormatter_Process_JSON ensures that the JSON output we get matches what
// we expect for the specified LogInput.
func TestEntryFormatter_Process_JSON(t *testing.T) {
ss := newStaticSalt(t)
expectedResultStr := fmt.Sprintf(testFormatJSONReqBasicStrFmt, ss.salt.GetIdentifiedHMAC("foo"))
issueTime, _ := time.Parse(time.RFC3339, "2020-05-28T13:40:18-05:00")
cases := map[string]struct {
Auth *logical.Auth
Req *logical.Request
Err error
Prefix string
ExpectedStr string
}{
"auth, request": {
&logical.Auth{
ClientToken: "foo",
Accessor: "bar",
DisplayName: "testtoken",
EntityID: "foobarentity",
NoDefaultPolicy: true,
Policies: []string{"root"},
TokenType: logical.TokenTypeService,
LeaseOptions: logical.LeaseOptions{
TTL: time.Hour * 4,
IssueTime: issueTime,
},
},
&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"},
},
},
errors.New("this is an error"),
"",
expectedResultStr,
},
"auth, request with prefix": {
&logical.Auth{
ClientToken: "foo",
Accessor: "bar",
EntityID: "foobarentity",
DisplayName: "testtoken",
NoDefaultPolicy: true,
Policies: []string{"root"},
TokenType: logical.TokenTypeService,
LeaseOptions: logical.LeaseOptions{
TTL: time.Hour * 4,
IssueTime: issueTime,
},
},
&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"},
},
},
errors.New("this is an error"),
"@cee: ",
expectedResultStr,
},
}
for name, tc := range cases {
cfg, err := NewFormatterConfig(WithHMACAccessor(false))
require.NoError(t, err)
formatter, err := NewEntryFormatter(cfg, ss, WithPrefix(tc.Prefix))
require.NoError(t, err)
in := &logical.LogInput{
Auth: tc.Auth,
Request: tc.Req,
OuterErr: tc.Err,
}
// Create an audit event and more generic eventlogger.event to allow us
// to process (format).
auditEvent, err := NewEvent(RequestType)
require.NoError(t, err)
auditEvent.Data = in
e := &eventlogger.Event{
Type: eventlogger.EventType(event.AuditType.String()),
CreatedAt: time.Now(),
Formatted: make(map[string][]byte),
Payload: auditEvent,
}
e2, err := formatter.Process(namespace.RootContext(nil), e)
require.NoErrorf(t, err, "bad: %s\nerr: %s", name, err)
jsonBytes, ok := e2.Format(JSONFormat.String())
require.True(t, ok)
require.Positive(t, len(jsonBytes))
if !strings.HasPrefix(string(jsonBytes), tc.Prefix) {
t.Fatalf("no prefix: %s \n log: %s\nprefix: %s", name, expectedResultStr, tc.Prefix)
}
expectedJSON := new(RequestEntry)
if err := jsonutil.DecodeJSON([]byte(expectedResultStr), &expectedJSON); err != nil {
t.Fatalf("bad json: %s", err)
}
expectedJSON.Request.Namespace = &Namespace{ID: "root"}
actualJSON := new(RequestEntry)
if err := jsonutil.DecodeJSON(jsonBytes[len(tc.Prefix):], &actualJSON); err != nil {
t.Fatalf("bad json: %s", err)
}
expectedJSON.Time = actualJSON.Time
expectedBytes, err := json.Marshal(expectedJSON)
if err != nil {
t.Fatalf("unable to marshal json: %s", err)
}
if !strings.HasSuffix(strings.TrimSpace(string(jsonBytes)), string(expectedBytes)) {
t.Fatalf("bad: %s\nResult:\n\n%q\n\nExpected:\n\n%q", name, string(jsonBytes), string(expectedBytes))
}
}
}
// TestEntryFormatter_Process_JSONx ensures that the JSONx output we get matches what
// we expect for the specified LogInput.
func TestEntryFormatter_Process_JSONx(t *testing.T) {
s, err := salt.NewSalt(context.Background(), nil, nil)
require.NoError(t, err)
tempStaticSalt := &staticSalt{salt: s}
fooSalted := s.GetIdentifiedHMAC("foo")
issueTime, _ := time.Parse(time.RFC3339, "2020-05-28T13:40:18-05:00")
cases := map[string]struct {
Auth *logical.Auth
Req *logical.Request
Err error
Prefix string
Result string
ExpectedStr string
}{
"auth, request": {
&logical.Auth{
ClientToken: "foo",
Accessor: "bar",
DisplayName: "testtoken",
EntityID: "foobarentity",
NoDefaultPolicy: true,
Policies: []string{"root"},
TokenType: logical.TokenTypeService,
LeaseOptions: logical.LeaseOptions{
TTL: time.Hour * 4,
IssueTime: issueTime,
},
},
&logical.Request{
ID: "request",
ClientToken: "foo",
ClientTokenAccessor: "bar",
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"},
},
PolicyOverride: true,
},
errors.New("this is an error"),
"",
"",
fmt.Sprintf(`<json:object name="auth"><json:string name="accessor">bar</json:string><json:string name="client_token">%s</json:string><json:string name="display_name">testtoken</json:string><json:string name="entity_id">foobarentity</json:string><json:boolean name="no_default_policy">true</json:boolean><json:array name="policies"><json:string>root</json:string></json:array><json:string name="token_issue_time">2020-05-28T13:40:18-05:00</json:string><json:number name="token_ttl">14400</json:number><json:string name="token_type">service</json:string></json:object><json:string name="error">this is an error</json:string><json:object name="request"><json:string name="client_token">%s</json:string><json:string name="client_token_accessor">bar</json:string><json:object name="headers"><json:array name="foo"><json:string>bar</json:string></json:array></json:object><json:string name="id">request</json:string><json:object name="namespace"><json:string name="id">root</json:string></json:object><json:string name="operation">update</json:string><json:string name="path">/foo</json:string><json:boolean name="policy_override">true</json:boolean><json:string name="remote_address">127.0.0.1</json:string><json:number name="wrap_ttl">60</json:number></json:object><json:string name="type">request</json:string>`,
fooSalted, fooSalted),
},
"auth, request with prefix": {
&logical.Auth{
ClientToken: "foo",
Accessor: "bar",
DisplayName: "testtoken",
NoDefaultPolicy: true,
EntityID: "foobarentity",
Policies: []string{"root"},
TokenType: logical.TokenTypeService,
LeaseOptions: logical.LeaseOptions{
TTL: time.Hour * 4,
IssueTime: issueTime,
},
},
&logical.Request{
ID: "request",
ClientToken: "foo",
ClientTokenAccessor: "bar",
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"},
},
PolicyOverride: true,
},
errors.New("this is an error"),
"",
"@cee: ",
fmt.Sprintf(`<json:object name="auth"><json:string name="accessor">bar</json:string><json:string name="client_token">%s</json:string><json:string name="display_name">testtoken</json:string><json:string name="entity_id">foobarentity</json:string><json:boolean name="no_default_policy">true</json:boolean><json:array name="policies"><json:string>root</json:string></json:array><json:string name="token_issue_time">2020-05-28T13:40:18-05:00</json:string><json:number name="token_ttl">14400</json:number><json:string name="token_type">service</json:string></json:object><json:string name="error">this is an error</json:string><json:object name="request"><json:string name="client_token">%s</json:string><json:string name="client_token_accessor">bar</json:string><json:object name="headers"><json:array name="foo"><json:string>bar</json:string></json:array></json:object><json:string name="id">request</json:string><json:object name="namespace"><json:string name="id">root</json:string></json:object><json:string name="operation">update</json:string><json:string name="path">/foo</json:string><json:boolean name="policy_override">true</json:boolean><json:string name="remote_address">127.0.0.1</json:string><json:number name="wrap_ttl">60</json:number></json:object><json:string name="type">request</json:string>`,
fooSalted, fooSalted),
},
}
for name, tc := range cases {
cfg, err := NewFormatterConfig(
WithOmitTime(true),
WithHMACAccessor(false),
WithFormat(JSONxFormat.String()),
)
require.NoError(t, err)
formatter, err := NewEntryFormatter(cfg, tempStaticSalt, WithPrefix(tc.Prefix))
require.NoError(t, err)
require.NotNil(t, formatter)
in := &logical.LogInput{
Auth: tc.Auth,
Request: tc.Req,
OuterErr: tc.Err,
}
// Create an audit event and more generic eventlogger.event to allow us
// to process (format).
auditEvent, err := NewEvent(RequestType)
require.NoError(t, err)
auditEvent.Data = in
e := &eventlogger.Event{
Type: eventlogger.EventType(event.AuditType.String()),
CreatedAt: time.Now(),
Formatted: make(map[string][]byte),
Payload: auditEvent,
}
e2, err := formatter.Process(namespace.RootContext(nil), e)
require.NoErrorf(t, err, "bad: %s\nerr: %s", name, err)
jsonxBytes, ok := e2.Format(JSONxFormat.String())
require.True(t, ok)
require.Positive(t, len(jsonxBytes))
if !strings.HasPrefix(string(jsonxBytes), tc.Prefix) {
t.Fatalf("no prefix: %s \n log: %s\nprefix: %s", name, tc.Result, tc.Prefix)
}
if !strings.HasSuffix(strings.TrimSpace(string(jsonxBytes)), string(tc.ExpectedStr)) {
t.Fatalf(
"bad: %s\nResult:\n\n%q\n\nExpected:\n\n%q",
name, strings.TrimSpace(string(jsonxBytes)), string(tc.ExpectedStr))
}
}
}
// TestEntryFormatter_FormatResponse_ElideListResponses ensures that we correctly
// elide data in responses to LIST operations.
func TestEntryFormatter_FormatResponse_ElideListResponses(t *testing.T) {
type test struct {
name string
inputData map[string]any
expectedData map[string]any
}
tests := map[string]struct {
inputData map[string]any
expectedData map[string]any
}{
"nil data": {
nil,
nil,
},
"Normal list (keys only)": {
map[string]any{
"keys": []string{"foo", "bar", "baz"},
},
map[string]any{
"keys": 3,
},
},
"Enhanced list (has key_info)": {
map[string]any{
"keys": []string{"foo", "bar", "baz", "quux"},
"key_info": map[string]any{
"foo": "alpha",
"bar": "beta",
"baz": "gamma",
"quux": "delta",
},
},
map[string]any{
"keys": 4,
"key_info": 4,
},
},
"Unconventional other values in a list response are not touched": {
map[string]any{
"keys": []string{"foo", "bar"},
"something_else": "baz",
},
map[string]any{
"keys": 2,
"something_else": "baz",
},
},
"Conventional values in a list response are not elided if their data types are unconventional": {
map[string]any{
"keys": map[string]any{
"You wouldn't expect keys to be a map": nil,
},
"key_info": []string{
"You wouldn't expect key_info to be a slice",
},
},
map[string]any{
"keys": map[string]any{
"You wouldn't expect keys to be a map": nil,
},
"key_info": []string{
"You wouldn't expect key_info to be a slice",
},
},
},
}
oneInterestingTestCase := tests["Enhanced list (has key_info)"]
ss := newStaticSalt(t)
ctx := namespace.RootContext(context.Background())
var formatter *EntryFormatter
var err error
format := func(t *testing.T, config FormatterConfig, operation logical.Operation, inputData map[string]any) *ResponseEntry {
formatter, err = NewEntryFormatter(config, ss)
require.NoError(t, err)
require.NotNil(t, formatter)
in := &logical.LogInput{
Request: &logical.Request{Operation: operation},
Response: &logical.Response{Data: inputData},
}
resp, err := formatter.FormatResponse(ctx, in)
require.NoError(t, err)
return resp
}
t.Run("Default case", func(t *testing.T) {
config, err := NewFormatterConfig(WithElision(true))
require.NoError(t, err)
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
entry := format(t, config, logical.ListOperation, tc.inputData)
assert.Equal(t, formatter.hashExpectedValueForComparison(tc.expectedData), entry.Response.Data)
})
}
})
t.Run("When Operation is not list, eliding does not happen", func(t *testing.T) {
config, err := NewFormatterConfig(WithElision(true))
require.NoError(t, err)
tc := oneInterestingTestCase
entry := format(t, config, logical.ReadOperation, tc.inputData)
assert.Equal(t, formatter.hashExpectedValueForComparison(tc.inputData), entry.Response.Data)
})
t.Run("When ElideListResponses is false, eliding does not happen", func(t *testing.T) {
config, err := NewFormatterConfig(WithElision(false), WithFormat(JSONFormat.String()))
require.NoError(t, err)
tc := oneInterestingTestCase
entry := format(t, config, logical.ListOperation, tc.inputData)
assert.Equal(t, formatter.hashExpectedValueForComparison(tc.inputData), entry.Response.Data)
})
t.Run("When Raw is true, eliding still happens", func(t *testing.T) {
config, err := NewFormatterConfig(WithElision(true), WithRaw(true), WithFormat(JSONFormat.String()))
require.NoError(t, err)
tc := oneInterestingTestCase
entry := format(t, config, logical.ListOperation, tc.inputData)
assert.Equal(t, tc.expectedData, entry.Response.Data)
})
}
// hashExpectedValueForComparison replicates enough of the audit HMAC process on a piece of expected data in a test,
// so that we can use assert.Equal to compare the expected and output values.
func (f *EntryFormatter) hashExpectedValueForComparison(input map[string]any) map[string]any {
// Copy input before modifying, since we may re-use the same data in another test
copied, err := copystructure.Copy(input)
if err != nil {
panic(err)
}
copiedAsMap := copied.(map[string]any)
s, err := f.salter.Salt(context.Background())
if err != nil {
panic(err)
}
err = hashMap(s.GetIdentifiedHMAC, copiedAsMap, nil)
if err != nil {
panic(err)
}
return copiedAsMap
}
// 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
}
// newStaticSalt returns a new staticSalt for use in testing.
func newStaticSalt(tb testing.TB) *staticSalt {
s, err := salt.NewSalt(context.Background(), nil, nil)
require.NoError(tb, err)
return &staticSalt{salt: s}
}
// staticSalt is a struct which can be used to obtain a static salt.
// a salt must be assigned when the struct is initialized.
type staticSalt struct {
salt *salt.Salt
}
// Salt returns the static salt and no error.
func (s *staticSalt) Salt(_ context.Context) (*salt.Salt, error) {
return s.salt, nil
}

View File

@@ -1,119 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package audit
import (
"context"
"errors"
"fmt"
"io"
"strings"
"github.com/hashicorp/vault/sdk/helper/salt"
"github.com/hashicorp/vault/sdk/logical"
)
var (
_ Formatter = (*EntryFormatterWriter)(nil)
_ Writer = (*EntryFormatterWriter)(nil)
)
// Salt returns a new salt with default configuration and no storage usage, and no error.
func (s *nonPersistentSalt) Salt(_ context.Context) (*salt.Salt, error) {
return salt.NewNonpersistentSalt(), nil
}
// NewEntryFormatterWriter should be used to create a new EntryFormatterWriter.
// Deprecated: Please move to using eventlogger.Event via EntryFormatter and a sink.
func NewEntryFormatterWriter(config FormatterConfig, formatter Formatter, writer Writer) (*EntryFormatterWriter, error) {
switch {
case formatter == nil:
return nil, errors.New("cannot create a new audit formatter writer with nil formatter")
case writer == nil:
return nil, errors.New("cannot create a new audit formatter writer with nil formatter")
}
fw := &EntryFormatterWriter{
Formatter: formatter,
Writer: writer,
config: config,
}
return fw, nil
}
// FormatAndWriteRequest attempts to format the specified logical.LogInput into an RequestEntry,
// and then write the request using the specified io.Writer.
// Deprecated: Please move to using eventlogger.Event via EntryFormatter and a sink.
func (f *EntryFormatterWriter) FormatAndWriteRequest(ctx context.Context, w io.Writer, in *logical.LogInput) error {
switch {
case in == nil || in.Request == nil:
return fmt.Errorf("request to request-audit a nil request")
case w == nil:
return fmt.Errorf("writer for audit request is nil")
case f.Formatter == nil:
return fmt.Errorf("no formatter specifed")
case f.Writer == nil:
return fmt.Errorf("no writer specified")
}
reqEntry, err := f.Formatter.FormatRequest(ctx, in)
if err != nil {
return err
}
return f.Writer.WriteRequest(w, reqEntry)
}
// FormatAndWriteResponse attempts to format the specified logical.LogInput into an ResponseEntry,
// and then write the response using the specified io.Writer.
// Deprecated: Please move to using eventlogger.Event via EntryFormatter and a sink.
func (f *EntryFormatterWriter) FormatAndWriteResponse(ctx context.Context, w io.Writer, in *logical.LogInput) error {
switch {
case in == nil || in.Request == nil:
return errors.New("request to response-audit a nil request")
case w == nil:
return errors.New("writer for audit request is nil")
case f.Formatter == nil:
return errors.New("no formatter specified")
case f.Writer == nil:
return errors.New("no writer specified")
}
respEntry, err := f.FormatResponse(ctx, in)
if err != nil {
return err
}
return f.Writer.WriteResponse(w, respEntry)
}
// NewTemporaryFormatter creates a formatter not backed by a persistent salt
func NewTemporaryFormatter(requiredFormat, prefix string) (*EntryFormatterWriter, error) {
cfg, err := NewFormatterConfig(WithFormat(requiredFormat))
if err != nil {
return nil, err
}
eventFormatter, err := NewEntryFormatter(cfg, &nonPersistentSalt{}, WithPrefix(prefix))
if err != nil {
return nil, err
}
var w Writer
switch {
case strings.EqualFold(requiredFormat, JSONxFormat.String()):
w = &JSONxWriter{Prefix: prefix}
default:
w = &JSONWriter{Prefix: prefix}
}
fw, err := NewEntryFormatterWriter(cfg, eventFormatter, w)
if err != nil {
return nil, err
}
return fw, nil
}

View File

@@ -1,410 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package audit
import (
"context"
"io"
"testing"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/helper/salt"
"github.com/hashicorp/vault/sdk/logical"
"github.com/mitchellh/copystructure"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
// newStaticSalt returns a new staticSalt for use in testing.
func newStaticSalt(tb testing.TB) *staticSalt {
s, err := salt.NewSalt(context.Background(), nil, nil)
require.NoError(tb, err)
return &staticSalt{salt: s}
}
// staticSalt is a struct which can be used to obtain a static salt.
// a salt must be assigned when the struct is initialized.
type staticSalt struct {
salt *salt.Salt
}
// Salt returns the static salt and no error.
func (s *staticSalt) Salt(_ context.Context) (*salt.Salt, error) {
return s.salt, nil
}
type testingFormatWriter struct {
salt *salt.Salt
lastRequest *RequestEntry
lastResponse *ResponseEntry
}
func (fw *testingFormatWriter) WriteRequest(_ io.Writer, entry *RequestEntry) error {
fw.lastRequest = entry
return nil
}
func (fw *testingFormatWriter) WriteResponse(_ io.Writer, entry *ResponseEntry) error {
fw.lastResponse = entry
return nil
}
func (fw *testingFormatWriter) Salt(ctx context.Context) (*salt.Salt, error) {
if fw.salt != nil {
return fw.salt, nil
}
var err error
fw.salt, err = salt.NewSalt(ctx, nil, nil)
if err != nil {
return nil, err
}
return fw.salt, nil
}
// hashExpectedValueForComparison replicates enough of the audit HMAC process on a piece of expected data in a test,
// so that we can use assert.Equal to compare the expected and output values.
func (fw *testingFormatWriter) hashExpectedValueForComparison(input map[string]interface{}) map[string]interface{} {
// Copy input before modifying, since we may re-use the same data in another test
copied, err := copystructure.Copy(input)
if err != nil {
panic(err)
}
copiedAsMap := copied.(map[string]interface{})
salter, err := fw.Salt(context.Background())
if err != nil {
panic(err)
}
err = hashMap(salter.GetIdentifiedHMAC, copiedAsMap, nil)
if err != nil {
panic(err)
}
return copiedAsMap
}
// TestNewEntryFormatterWriter tests that creating a new EntryFormatterWriter can be done safely.
func TestNewEntryFormatterWriter(t *testing.T) {
tests := map[string]struct {
Salter Salter
UseStaticSalter bool
UseNilFormatter bool
UseNilWriter bool
IsErrorExpected bool
ExpectedErrorMessage string
}{
"nil": {
Salter: nil,
UseNilFormatter: true,
UseNilWriter: true,
IsErrorExpected: true,
ExpectedErrorMessage: "cannot create a new audit formatter with nil salter",
},
"static": {
UseStaticSalter: true,
IsErrorExpected: false,
},
}
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
var s Salter
switch {
case tc.UseStaticSalter:
s = newStaticSalt(t)
default:
s = tc.Salter
}
cfg, err := NewFormatterConfig()
require.NoError(t, err)
var f Formatter
if !tc.UseNilFormatter {
tempFormatter, err := NewEntryFormatter(cfg, s)
require.NoError(t, err)
require.NotNil(t, tempFormatter)
f = tempFormatter
}
var w Writer
if !tc.UseNilWriter {
w = &JSONWriter{}
}
fw, err := NewEntryFormatterWriter(cfg, f, w)
switch {
case tc.IsErrorExpected:
require.Error(t, err)
require.Nil(t, fw)
default:
require.NoError(t, err)
require.NotNil(t, fw)
}
})
}
}
// TestEntryFormatter_FormatRequest exercises EntryFormatter.FormatRequest with
// varying inputs.
func TestEntryFormatter_FormatRequest(t *testing.T) {
tests := map[string]struct {
Input *logical.LogInput
IsErrorExpected bool
ExpectedErrorMessage string
RootNamespace bool
}{
"nil": {
Input: nil,
IsErrorExpected: true,
ExpectedErrorMessage: "request to request-audit a nil request",
},
"basic-input": {
Input: &logical.LogInput{},
IsErrorExpected: true,
ExpectedErrorMessage: "request to request-audit a nil request",
},
"input-and-request-no-ns": {
Input: &logical.LogInput{Request: &logical.Request{ID: "123"}},
IsErrorExpected: true,
ExpectedErrorMessage: "no namespace",
RootNamespace: false,
},
"input-and-request-with-ns": {
Input: &logical.LogInput{Request: &logical.Request{ID: "123"}},
IsErrorExpected: false,
RootNamespace: true,
},
}
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
ss := newStaticSalt(t)
cfg, err := NewFormatterConfig()
require.NoError(t, err)
f, err := NewEntryFormatter(cfg, ss)
require.NoError(t, err)
var ctx context.Context
switch {
case tc.RootNamespace:
ctx = namespace.RootContext(context.Background())
default:
ctx = context.Background()
}
entry, err := f.FormatRequest(ctx, tc.Input)
switch {
case tc.IsErrorExpected:
require.Error(t, err)
require.EqualError(t, err, tc.ExpectedErrorMessage)
require.Nil(t, entry)
default:
require.NoError(t, err)
require.NotNil(t, entry)
}
})
}
}
// TestEntryFormatter_FormatResponse exercises EntryFormatter.FormatResponse with
// varying inputs.
func TestEntryFormatter_FormatResponse(t *testing.T) {
tests := map[string]struct {
Input *logical.LogInput
IsErrorExpected bool
ExpectedErrorMessage string
RootNamespace bool
}{
"nil": {
Input: nil,
IsErrorExpected: true,
ExpectedErrorMessage: "request to response-audit a nil request",
},
"basic-input": {
Input: &logical.LogInput{},
IsErrorExpected: true,
ExpectedErrorMessage: "request to response-audit a nil request",
},
"input-and-request-no-ns": {
Input: &logical.LogInput{Request: &logical.Request{ID: "123"}},
IsErrorExpected: true,
ExpectedErrorMessage: "no namespace",
RootNamespace: false,
},
"input-and-request-with-ns": {
Input: &logical.LogInput{Request: &logical.Request{ID: "123"}},
IsErrorExpected: false,
RootNamespace: true,
},
}
for name, tc := range tests {
name := name
tc := tc
t.Run(name, func(t *testing.T) {
t.Parallel()
ss := newStaticSalt(t)
cfg, err := NewFormatterConfig()
require.NoError(t, err)
f, err := NewEntryFormatter(cfg, ss)
require.NoError(t, err)
var ctx context.Context
switch {
case tc.RootNamespace:
ctx = namespace.RootContext(context.Background())
default:
ctx = context.Background()
}
entry, err := f.FormatResponse(ctx, tc.Input)
switch {
case tc.IsErrorExpected:
require.Error(t, err)
require.EqualError(t, err, tc.ExpectedErrorMessage)
require.Nil(t, entry)
default:
require.NoError(t, err)
require.NotNil(t, entry)
}
})
}
}
func TestElideListResponses(t *testing.T) {
type test struct {
name string
inputData map[string]interface{}
expectedData map[string]interface{}
}
tests := []test{
{
"nil data",
nil,
nil,
},
{
"Normal list (keys only)",
map[string]interface{}{
"keys": []string{"foo", "bar", "baz"},
},
map[string]interface{}{
"keys": 3,
},
},
{
"Enhanced list (has key_info)",
map[string]interface{}{
"keys": []string{"foo", "bar", "baz", "quux"},
"key_info": map[string]interface{}{
"foo": "alpha",
"bar": "beta",
"baz": "gamma",
"quux": "delta",
},
},
map[string]interface{}{
"keys": 4,
"key_info": 4,
},
},
{
"Unconventional other values in a list response are not touched",
map[string]interface{}{
"keys": []string{"foo", "bar"},
"something_else": "baz",
},
map[string]interface{}{
"keys": 2,
"something_else": "baz",
},
},
{
"Conventional values in a list response are not elided if their data types are unconventional",
map[string]interface{}{
"keys": map[string]interface{}{
"You wouldn't expect keys to be a map": nil,
},
"key_info": []string{
"You wouldn't expect key_info to be a slice",
},
},
map[string]interface{}{
"keys": map[string]interface{}{
"You wouldn't expect keys to be a map": nil,
},
"key_info": []string{
"You wouldn't expect key_info to be a slice",
},
},
},
}
oneInterestingTestCase := tests[2]
tfw := testingFormatWriter{}
ctx := namespace.RootContext(context.Background())
formatResponse := func(t *testing.T, config FormatterConfig, operation logical.Operation, inputData map[string]interface{},
) {
f, err := NewEntryFormatter(config, &tfw)
require.NoError(t, err)
formatter, err := NewEntryFormatterWriter(config, f, &tfw)
require.NoError(t, err)
require.NotNil(t, formatter)
err = formatter.FormatAndWriteResponse(ctx, io.Discard, &logical.LogInput{
Request: &logical.Request{Operation: operation},
Response: &logical.Response{Data: inputData},
})
require.Nil(t, err)
}
t.Run("Default case", func(t *testing.T) {
config, err := NewFormatterConfig(WithElision(true))
require.NoError(t, err)
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
formatResponse(t, config, logical.ListOperation, tc.inputData)
assert.Equal(t, tfw.hashExpectedValueForComparison(tc.expectedData), tfw.lastResponse.Response.Data)
})
}
})
t.Run("When Operation is not list, eliding does not happen", func(t *testing.T) {
config, err := NewFormatterConfig(WithElision(true))
require.NoError(t, err)
tc := oneInterestingTestCase
formatResponse(t, config, logical.ReadOperation, tc.inputData)
assert.Equal(t, tfw.hashExpectedValueForComparison(tc.inputData), tfw.lastResponse.Response.Data)
})
t.Run("When ElideListResponses is false, eliding does not happen", func(t *testing.T) {
config, err := NewFormatterConfig(WithElision(false), WithFormat(JSONFormat.String()))
require.NoError(t, err)
tc := oneInterestingTestCase
formatResponse(t, config, logical.ListOperation, tc.inputData)
assert.Equal(t, tfw.hashExpectedValueForComparison(tc.inputData), tfw.lastResponse.Response.Data)
})
t.Run("When Raw is true, eliding still happens", func(t *testing.T) {
config, err := NewFormatterConfig(WithElision(true), WithRaw(true), WithFormat(JSONFormat.String()))
require.NoError(t, err)
tc := oneInterestingTestCase
formatResponse(t, config, logical.ListOperation, tc.inputData)
assert.Equal(t, tc.expectedData, tfw.lastResponse.Response.Data)
})
}

View File

@@ -1,49 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package audit
import (
"encoding/json"
"fmt"
"io"
)
var _ Writer = (*JSONWriter)(nil)
// JSONWriter is a Writer implementation that structures data into a JSON format.
type JSONWriter struct {
Prefix string
}
func (f *JSONWriter) WriteRequest(w io.Writer, req *RequestEntry) error {
if req == nil {
return fmt.Errorf("request entry was nil, cannot encode")
}
if len(f.Prefix) > 0 {
_, err := w.Write([]byte(f.Prefix))
if err != nil {
return err
}
}
enc := json.NewEncoder(w)
return enc.Encode(req)
}
func (f *JSONWriter) WriteResponse(w io.Writer, resp *ResponseEntry) error {
if resp == nil {
return fmt.Errorf("response entry was nil, cannot encode")
}
if len(f.Prefix) > 0 {
_, err := w.Write([]byte(f.Prefix))
if err != nil {
return err
}
}
enc := json.NewEncoder(w)
return enc.Encode(resp)
}

View File

@@ -1,154 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package audit
import (
"bytes"
"encoding/json"
"errors"
"fmt"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/helper/jsonutil"
"github.com/hashicorp/vault/sdk/logical"
)
func TestFormatJSON_formatRequest(t *testing.T) {
ss := newStaticSalt(t)
expectedResultStr := fmt.Sprintf(testFormatJSONReqBasicStrFmt, ss.salt.GetIdentifiedHMAC("foo"))
issueTime, _ := time.Parse(time.RFC3339, "2020-05-28T13:40:18-05:00")
cases := map[string]struct {
Auth *logical.Auth
Req *logical.Request
Err error
Prefix string
ExpectedStr string
}{
"auth, request": {
&logical.Auth{
ClientToken: "foo",
Accessor: "bar",
DisplayName: "testtoken",
EntityID: "foobarentity",
NoDefaultPolicy: true,
Policies: []string{"root"},
TokenType: logical.TokenTypeService,
LeaseOptions: logical.LeaseOptions{
TTL: time.Hour * 4,
IssueTime: issueTime,
},
},
&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"},
},
},
errors.New("this is an error"),
"",
expectedResultStr,
},
"auth, request with prefix": {
&logical.Auth{
ClientToken: "foo",
Accessor: "bar",
EntityID: "foobarentity",
DisplayName: "testtoken",
NoDefaultPolicy: true,
Policies: []string{"root"},
TokenType: logical.TokenTypeService,
LeaseOptions: logical.LeaseOptions{
TTL: time.Hour * 4,
IssueTime: issueTime,
},
},
&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"},
},
},
errors.New("this is an error"),
"@cee: ",
expectedResultStr,
},
}
for name, tc := range cases {
var buf bytes.Buffer
cfg, err := NewFormatterConfig(WithHMACAccessor(false))
require.NoError(t, err)
f, err := NewEntryFormatter(cfg, ss)
require.NoError(t, err)
formatter := EntryFormatterWriter{
Formatter: f,
Writer: &JSONWriter{
Prefix: tc.Prefix,
},
config: cfg,
}
in := &logical.LogInput{
Auth: tc.Auth,
Request: tc.Req,
OuterErr: tc.Err,
}
err = formatter.FormatAndWriteRequest(namespace.RootContext(nil), &buf, in)
require.NoErrorf(t, err, "bad: %s\nerr: %s", name, err)
if !strings.HasPrefix(buf.String(), tc.Prefix) {
t.Fatalf("no prefix: %s \n log: %s\nprefix: %s", name, expectedResultStr, tc.Prefix)
}
expectedJSON := new(RequestEntry)
if err := jsonutil.DecodeJSON([]byte(expectedResultStr), &expectedJSON); err != nil {
t.Fatalf("bad json: %s", err)
}
expectedJSON.Request.Namespace = &Namespace{ID: "root"}
actualjson := new(RequestEntry)
if err := jsonutil.DecodeJSON([]byte(buf.String())[len(tc.Prefix):], &actualjson); err != nil {
t.Fatalf("bad json: %s", err)
}
expectedJSON.Time = actualjson.Time
expectedBytes, err := json.Marshal(expectedJSON)
if err != nil {
t.Fatalf("unable to marshal json: %s", err)
}
if !strings.HasSuffix(strings.TrimSpace(buf.String()), string(expectedBytes)) {
t.Fatalf(
"bad: %s\nResult:\n\n%q\n\nExpected:\n\n%q",
name, buf.String(), string(expectedBytes))
}
}
}
const testFormatJSONReqBasicStrFmt = `{"time":"2015-08-05T13:45:46Z","type":"request","auth":{"client_token":"%s","accessor":"bar","display_name":"testtoken","policies":["root"],"no_default_policy":true,"metadata":null,"entity_id":"foobarentity","token_type":"service", "token_ttl": 14400, "token_issue_time": "2020-05-28T13:40:18-05:00"},"request":{"operation":"update","path":"/foo","data":null,"wrap_ttl":60,"remote_address":"127.0.0.1","headers":{"foo":["bar"]}},"error":"this is an error"}
`

View File

@@ -1,71 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package audit
import (
"encoding/json"
"fmt"
"io"
"github.com/jefferai/jsonx"
)
var _ Writer = (*JSONxWriter)(nil)
// JSONxWriter is a Writer implementation that structures data into an XML format.
type JSONxWriter struct {
Prefix string
}
func (f *JSONxWriter) WriteRequest(w io.Writer, req *RequestEntry) error {
if req == nil {
return fmt.Errorf("request entry was nil, cannot encode")
}
if len(f.Prefix) > 0 {
_, err := w.Write([]byte(f.Prefix))
if err != nil {
return err
}
}
jsonBytes, err := json.Marshal(req)
if err != nil {
return err
}
xmlBytes, err := jsonx.EncodeJSONBytes(jsonBytes)
if err != nil {
return err
}
_, err = w.Write(xmlBytes)
return err
}
func (f *JSONxWriter) WriteResponse(w io.Writer, resp *ResponseEntry) error {
if resp == nil {
return fmt.Errorf("response entry was nil, cannot encode")
}
if len(f.Prefix) > 0 {
_, err := w.Write([]byte(f.Prefix))
if err != nil {
return err
}
}
jsonBytes, err := json.Marshal(resp)
if err != nil {
return err
}
xmlBytes, err := jsonx.EncodeJSONBytes(jsonBytes)
if err != nil {
return err
}
_, err = w.Write(xmlBytes)
return err
}

View File

@@ -1,148 +0,0 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package audit
import (
"bytes"
"context"
"errors"
"fmt"
"strings"
"testing"
"time"
"github.com/stretchr/testify/require"
"github.com/hashicorp/vault/helper/namespace"
"github.com/hashicorp/vault/sdk/helper/salt"
"github.com/hashicorp/vault/sdk/logical"
)
func TestFormatJSONx_formatRequest(t *testing.T) {
s, err := salt.NewSalt(context.Background(), nil, nil)
require.NoError(t, err)
tempStaticSalt := &staticSalt{salt: s}
fooSalted := s.GetIdentifiedHMAC("foo")
issueTime, _ := time.Parse(time.RFC3339, "2020-05-28T13:40:18-05:00")
cases := map[string]struct {
Auth *logical.Auth
Req *logical.Request
Err error
Prefix string
Result string
ExpectedStr string
}{
"auth, request": {
&logical.Auth{
ClientToken: "foo",
Accessor: "bar",
DisplayName: "testtoken",
EntityID: "foobarentity",
NoDefaultPolicy: true,
Policies: []string{"root"},
TokenType: logical.TokenTypeService,
LeaseOptions: logical.LeaseOptions{
TTL: time.Hour * 4,
IssueTime: issueTime,
},
},
&logical.Request{
ID: "request",
ClientToken: "foo",
ClientTokenAccessor: "bar",
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"},
},
PolicyOverride: true,
},
errors.New("this is an error"),
"",
"",
fmt.Sprintf(`<json:object name="auth"><json:string name="accessor">bar</json:string><json:string name="client_token">%s</json:string><json:string name="display_name">testtoken</json:string><json:string name="entity_id">foobarentity</json:string><json:boolean name="no_default_policy">true</json:boolean><json:array name="policies"><json:string>root</json:string></json:array><json:string name="token_issue_time">2020-05-28T13:40:18-05:00</json:string><json:number name="token_ttl">14400</json:number><json:string name="token_type">service</json:string></json:object><json:string name="error">this is an error</json:string><json:object name="request"><json:string name="client_token">%s</json:string><json:string name="client_token_accessor">bar</json:string><json:object name="headers"><json:array name="foo"><json:string>bar</json:string></json:array></json:object><json:string name="id">request</json:string><json:object name="namespace"><json:string name="id">root</json:string></json:object><json:string name="operation">update</json:string><json:string name="path">/foo</json:string><json:boolean name="policy_override">true</json:boolean><json:string name="remote_address">127.0.0.1</json:string><json:number name="wrap_ttl">60</json:number></json:object><json:string name="type">request</json:string>`,
fooSalted, fooSalted),
},
"auth, request with prefix": {
&logical.Auth{
ClientToken: "foo",
Accessor: "bar",
DisplayName: "testtoken",
NoDefaultPolicy: true,
EntityID: "foobarentity",
Policies: []string{"root"},
TokenType: logical.TokenTypeService,
LeaseOptions: logical.LeaseOptions{
TTL: time.Hour * 4,
IssueTime: issueTime,
},
},
&logical.Request{
ID: "request",
ClientToken: "foo",
ClientTokenAccessor: "bar",
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"},
},
PolicyOverride: true,
},
errors.New("this is an error"),
"",
"@cee: ",
fmt.Sprintf(`<json:object name="auth"><json:string name="accessor">bar</json:string><json:string name="client_token">%s</json:string><json:string name="display_name">testtoken</json:string><json:string name="entity_id">foobarentity</json:string><json:boolean name="no_default_policy">true</json:boolean><json:array name="policies"><json:string>root</json:string></json:array><json:string name="token_issue_time">2020-05-28T13:40:18-05:00</json:string><json:number name="token_ttl">14400</json:number><json:string name="token_type">service</json:string></json:object><json:string name="error">this is an error</json:string><json:object name="request"><json:string name="client_token">%s</json:string><json:string name="client_token_accessor">bar</json:string><json:object name="headers"><json:array name="foo"><json:string>bar</json:string></json:array></json:object><json:string name="id">request</json:string><json:object name="namespace"><json:string name="id">root</json:string></json:object><json:string name="operation">update</json:string><json:string name="path">/foo</json:string><json:boolean name="policy_override">true</json:boolean><json:string name="remote_address">127.0.0.1</json:string><json:number name="wrap_ttl">60</json:number></json:object><json:string name="type">request</json:string>`,
fooSalted, fooSalted),
},
}
for name, tc := range cases {
var buf bytes.Buffer
cfg, err := NewFormatterConfig(
WithOmitTime(true),
WithHMACAccessor(false),
WithFormat(JSONxFormat.String()),
)
require.NoError(t, err)
f, err := NewEntryFormatter(cfg, tempStaticSalt)
require.NoError(t, err)
writer := &JSONxWriter{Prefix: tc.Prefix}
formatter, err := NewEntryFormatterWriter(cfg, f, writer)
require.NoError(t, err)
require.NotNil(t, formatter)
in := &logical.LogInput{
Auth: tc.Auth,
Request: tc.Req,
OuterErr: tc.Err,
}
if err := formatter.FormatAndWriteRequest(namespace.RootContext(nil), &buf, in); err != nil {
t.Fatalf("bad: %s\nerr: %s", name, err)
}
if !strings.HasPrefix(buf.String(), tc.Prefix) {
t.Fatalf("no prefix: %s \n log: %s\nprefix: %s", name, tc.Result, tc.Prefix)
}
if !strings.HasSuffix(strings.TrimSpace(buf.String()), string(tc.ExpectedStr)) {
t.Fatalf(
"bad: %s\nResult:\n\n%q\n\nExpected:\n\n%q",
name, strings.TrimSpace(buf.String()), string(tc.ExpectedStr))
}
}
}