mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-29 17:52:32 +00:00
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:
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"}
|
||||
`
|
||||
@@ -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
|
||||
}
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user