mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 02:02:43 +00:00
VAULT-17080: audit formatter node (JSON) (#21769)
* Export AuditFormatter, improve tests * Correct issues in 'Date' for tests
This commit is contained in:
@@ -44,13 +44,13 @@ type Writer interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
_ Formatter = (*auditFormatter)(nil)
|
_ Formatter = (*AuditFormatter)(nil)
|
||||||
_ Formatter = (*AuditFormatterWriter)(nil)
|
_ Formatter = (*AuditFormatterWriter)(nil)
|
||||||
_ Writer = (*AuditFormatterWriter)(nil)
|
_ Writer = (*AuditFormatterWriter)(nil)
|
||||||
)
|
)
|
||||||
|
|
||||||
// auditFormatter should be used to format audit requests and responses.
|
// AuditFormatter should be used to format audit requests and responses.
|
||||||
type auditFormatter struct {
|
type AuditFormatter struct {
|
||||||
salter Salter
|
salter Salter
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,13 +98,13 @@ func (s *nonPersistentSalt) Salt(_ context.Context) (*salt.Salt, error) {
|
|||||||
return salt.NewNonpersistentSalt(), nil
|
return salt.NewNonpersistentSalt(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuditFormatter should be used to create an auditFormatter.
|
// NewAuditFormatter should be used to create an AuditFormatter.
|
||||||
func NewAuditFormatter(salter Salter) (*auditFormatter, error) {
|
func NewAuditFormatter(salter Salter) (*AuditFormatter, error) {
|
||||||
if salter == nil {
|
if salter == nil {
|
||||||
return nil, errors.New("cannot create a new audit formatter with nil salter")
|
return nil, errors.New("cannot create a new audit formatter with nil salter")
|
||||||
}
|
}
|
||||||
|
|
||||||
return &auditFormatter{salter: salter}, nil
|
return &AuditFormatter{salter: salter}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuditFormatterWriter should be used to create a new AuditFormatterWriter.
|
// NewAuditFormatterWriter should be used to create a new AuditFormatterWriter.
|
||||||
@@ -125,10 +125,10 @@ func NewAuditFormatterWriter(formatter Formatter, writer Writer) (*AuditFormatte
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FormatRequest attempts to format the specified logical.LogInput into an AuditRequestEntry.
|
// FormatRequest attempts to format the specified logical.LogInput into an AuditRequestEntry.
|
||||||
func (f *auditFormatter) FormatRequest(ctx context.Context, config FormatterConfig, in *logical.LogInput) (*AuditRequestEntry, error) {
|
func (f *AuditFormatter) FormatRequest(ctx context.Context, config FormatterConfig, in *logical.LogInput) (*AuditRequestEntry, error) {
|
||||||
switch {
|
switch {
|
||||||
case in == nil || in.Request == nil:
|
case in == nil || in.Request == nil:
|
||||||
return nil, errors.New("request to response-audit a nil request")
|
return nil, errors.New("request to request-audit a nil request")
|
||||||
case f.salter == nil:
|
case f.salter == nil:
|
||||||
return nil, errors.New("salt func not configured")
|
return nil, errors.New("salt func not configured")
|
||||||
}
|
}
|
||||||
@@ -255,7 +255,7 @@ func (f *auditFormatter) FormatRequest(ctx context.Context, config FormatterConf
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FormatResponse attempts to format the specified logical.LogInput into an AuditResponseEntry.
|
// FormatResponse attempts to format the specified logical.LogInput into an AuditResponseEntry.
|
||||||
func (f *auditFormatter) FormatResponse(ctx context.Context, config FormatterConfig, in *logical.LogInput) (*AuditResponseEntry, error) {
|
func (f *AuditFormatter) FormatResponse(ctx context.Context, config FormatterConfig, in *logical.LogInput) (*AuditResponseEntry, error) {
|
||||||
switch {
|
switch {
|
||||||
case in == nil || in.Request == nil:
|
case in == nil || in.Request == nil:
|
||||||
return nil, errors.New("request to response-audit a nil request")
|
return nil, errors.New("request to response-audit a nil request")
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ package audit
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -17,27 +16,13 @@ import (
|
|||||||
|
|
||||||
"github.com/hashicorp/vault/helper/namespace"
|
"github.com/hashicorp/vault/helper/namespace"
|
||||||
"github.com/hashicorp/vault/sdk/helper/jsonutil"
|
"github.com/hashicorp/vault/sdk/helper/jsonutil"
|
||||||
"github.com/hashicorp/vault/sdk/helper/salt"
|
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 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(ctx context.Context) (*salt.Salt, error) {
|
|
||||||
return s.salt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFormatJSON_formatRequest(t *testing.T) {
|
func TestFormatJSON_formatRequest(t *testing.T) {
|
||||||
s, err := salt.NewSalt(context.Background(), nil, nil)
|
ss := newStaticSalt(t)
|
||||||
require.NoError(t, err)
|
|
||||||
tempStaticSalt := &staticSalt{salt: s}
|
|
||||||
|
|
||||||
expectedResultStr := fmt.Sprintf(testFormatJSONReqBasicStrFmt, s.GetIdentifiedHMAC("foo"))
|
expectedResultStr := fmt.Sprintf(testFormatJSONReqBasicStrFmt, ss.salt.GetIdentifiedHMAC("foo"))
|
||||||
|
|
||||||
issueTime, _ := time.Parse(time.RFC3339, "2020-05-28T13:40:18-05:00")
|
issueTime, _ := time.Parse(time.RFC3339, "2020-05-28T13:40:18-05:00")
|
||||||
cases := map[string]struct {
|
cases := map[string]struct {
|
||||||
@@ -113,7 +98,7 @@ func TestFormatJSON_formatRequest(t *testing.T) {
|
|||||||
|
|
||||||
for name, tc := range cases {
|
for name, tc := range cases {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
f, err := NewAuditFormatter(tempStaticSalt)
|
f, err := NewAuditFormatter(ss)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
formatter := AuditFormatterWriter{
|
formatter := AuditFormatterWriter{
|
||||||
Formatter: f,
|
Formatter: f,
|
||||||
|
|||||||
@@ -16,6 +16,25 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// newStaticSalt returns a new staticSalt for use in testing.
|
||||||
|
func newStaticSalt(t *testing.T) *staticSalt {
|
||||||
|
s, err := salt.NewSalt(context.Background(), nil, nil)
|
||||||
|
require.NoError(t, 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 {
|
type testingFormatWriter struct {
|
||||||
salt *salt.Salt
|
salt *salt.Salt
|
||||||
lastRequest *AuditRequestEntry
|
lastRequest *AuditRequestEntry
|
||||||
@@ -67,66 +86,178 @@ func (fw *testingFormatWriter) hashExpectedValueForComparison(input map[string]i
|
|||||||
return copiedAsMap
|
return copiedAsMap
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuditFormat_FormatRequest_Errors(t *testing.T) {
|
// TestNewAuditFormatter tests that creating a new AuditFormatter can be done safely.
|
||||||
config := FormatterConfig{}
|
func TestNewAuditFormatter(t *testing.T) {
|
||||||
formatter := auditFormatter{}
|
tests := map[string]struct {
|
||||||
|
Salter Salter
|
||||||
entry, err := formatter.FormatRequest(context.Background(), config, &logical.LogInput{})
|
UseStaticSalter bool
|
||||||
require.Error(t, err)
|
IsErrorExpected bool
|
||||||
require.Nil(t, entry)
|
ExpectedErrorMessag string
|
||||||
}
|
}{
|
||||||
|
"nil": {
|
||||||
func TestAuditFormatWriter_FormatRequest_Errors(t *testing.T) {
|
Salter: nil,
|
||||||
config := FormatterConfig{}
|
IsErrorExpected: true,
|
||||||
formatter := AuditFormatterWriter{
|
ExpectedErrorMessag: "cannot create a new audit formatter with nil salter",
|
||||||
Formatter: &auditFormatter{},
|
},
|
||||||
Writer: &testingFormatWriter{},
|
"static": {
|
||||||
|
UseStaticSalter: true,
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedErrorMessag: "",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err := formatter.FormatRequest(context.Background(), config, &logical.LogInput{})
|
for name, tc := range tests {
|
||||||
require.Error(t, err)
|
name := name
|
||||||
require.Nil(t, entry)
|
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
|
||||||
|
}
|
||||||
|
|
||||||
in := &logical.LogInput{
|
f, err := NewAuditFormatter(s)
|
||||||
Request: &logical.Request{},
|
switch {
|
||||||
|
case tc.IsErrorExpected:
|
||||||
|
require.Error(t, err)
|
||||||
|
require.Nil(t, f)
|
||||||
|
default:
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, f)
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err = formatter.FormatRequest(context.Background(), config, in)
|
|
||||||
require.Error(t, err)
|
|
||||||
require.Nil(t, entry)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAuditFormat_FormatResponse_Errors(t *testing.T) {
|
// TestAuditFormatter_FormatRequest exercises AuditFormatter.FormatRequest with
|
||||||
config := FormatterConfig{}
|
// varying inputs.
|
||||||
formatter := auditFormatter{}
|
func TestAuditFormatter_FormatRequest(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
entry, err := formatter.FormatResponse(context.Background(), config, &logical.LogInput{})
|
Input *logical.LogInput
|
||||||
require.Error(t, err)
|
IsErrorExpected bool
|
||||||
require.Nil(t, entry)
|
ExpectedErrorMessage string
|
||||||
|
RootNamespace bool
|
||||||
in := &logical.LogInput{Request: &logical.Request{}}
|
}{
|
||||||
|
"nil": {
|
||||||
entry, err = formatter.FormatResponse(context.Background(), config, in)
|
Input: nil,
|
||||||
require.Error(t, err)
|
IsErrorExpected: true,
|
||||||
require.Nil(t, entry)
|
ExpectedErrorMessage: "request to request-audit a nil request",
|
||||||
}
|
},
|
||||||
|
"basic-input": {
|
||||||
func TestAuditFormatWriter_FormatResponse_Errors(t *testing.T) {
|
Input: &logical.LogInput{},
|
||||||
config := FormatterConfig{}
|
IsErrorExpected: true,
|
||||||
formatter := AuditFormatterWriter{
|
ExpectedErrorMessage: "request to request-audit a nil request",
|
||||||
Formatter: &auditFormatter{},
|
},
|
||||||
Writer: &testingFormatWriter{},
|
"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,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err := formatter.FormatResponse(context.Background(), config, &logical.LogInput{})
|
for name, tc := range tests {
|
||||||
require.Error(t, err)
|
name := name
|
||||||
require.Nil(t, entry)
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
config := FormatterConfig{}
|
||||||
|
f, err := NewAuditFormatter(newStaticSalt(t))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
in := &logical.LogInput{Request: &logical.Request{}}
|
var ctx context.Context
|
||||||
|
switch {
|
||||||
|
case tc.RootNamespace:
|
||||||
|
ctx = namespace.RootContext(context.Background())
|
||||||
|
default:
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
entry, err = formatter.FormatResponse(context.Background(), config, in)
|
entry, err := f.FormatRequest(ctx, config, tc.Input)
|
||||||
require.Error(t, err)
|
|
||||||
require.Nil(t, entry)
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAuditFormatter_FormatResponse exercises AuditFormatter.FormatResponse with
|
||||||
|
// varying inputs.
|
||||||
|
func TestAuditFormatter_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()
|
||||||
|
config := FormatterConfig{}
|
||||||
|
f, err := NewAuditFormatter(newStaticSalt(t))
|
||||||
|
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, config, 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) {
|
func TestElideListResponses(t *testing.T) {
|
||||||
|
|||||||
@@ -42,11 +42,11 @@ func TestAuditEvent_New(t *testing.T) {
|
|||||||
WithID("audit_123"),
|
WithID("audit_123"),
|
||||||
WithFormat(string(AuditFormatJSON)),
|
WithFormat(string(AuditFormatJSON)),
|
||||||
WithSubtype(string(AuditResponse)),
|
WithSubtype(string(AuditResponse)),
|
||||||
WithNow(time.Date(2023, time.July, 4, 12, 0o3, 0o0, 0o0, &time.Location{})),
|
WithNow(time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local)),
|
||||||
},
|
},
|
||||||
IsErrorExpected: false,
|
IsErrorExpected: false,
|
||||||
ExpectedID: "audit_123",
|
ExpectedID: "audit_123",
|
||||||
ExpectedTimestamp: time.Date(2023, time.July, 4, 12, 0o3, 0o0, 0o0, &time.Location{}),
|
ExpectedTimestamp: time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local),
|
||||||
ExpectedSubtype: AuditResponse,
|
ExpectedSubtype: AuditResponse,
|
||||||
ExpectedFormat: AuditFormatJSON,
|
ExpectedFormat: AuditFormatJSON,
|
||||||
},
|
},
|
||||||
|
|||||||
103
internal/observability/event/node_formatter_audit_json.go
Normal file
103
internal/observability/event/node_formatter_audit_json.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
vaultaudit "github.com/hashicorp/vault/audit"
|
||||||
|
"github.com/hashicorp/vault/sdk/helper/jsonutil"
|
||||||
|
|
||||||
|
"github.com/hashicorp/eventlogger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ eventlogger.Node = (*AuditFormatterJSON)(nil)
|
||||||
|
|
||||||
|
// AuditFormatterJSON represents the formatter node which is used to handle
|
||||||
|
// formatting audit events as JSON.
|
||||||
|
type AuditFormatterJSON struct {
|
||||||
|
config vaultaudit.FormatterConfig
|
||||||
|
format auditFormat
|
||||||
|
formatter vaultaudit.Formatter
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewAuditFormatterJSON should be used to create an AuditFormatterJSON.
|
||||||
|
func NewAuditFormatterJSON(config vaultaudit.FormatterConfig, salter vaultaudit.Salter) (*AuditFormatterJSON, error) {
|
||||||
|
const op = "event.NewAuditFormatterJSON"
|
||||||
|
|
||||||
|
f, err := vaultaudit.NewAuditFormatter(salter)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: unable to create new JSON audit formatter: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonFormatter := &AuditFormatterJSON{
|
||||||
|
format: AuditFormatJSON,
|
||||||
|
config: config,
|
||||||
|
formatter: f,
|
||||||
|
}
|
||||||
|
|
||||||
|
return jsonFormatter, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reopen is a no-op for a formatter node.
|
||||||
|
func (_ *AuditFormatterJSON) Reopen() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type describes the type of this node (formatter).
|
||||||
|
func (_ *AuditFormatterJSON) Type() eventlogger.NodeType {
|
||||||
|
return eventlogger.NodeTypeFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process will attempt to parse the incoming event data into a corresponding
|
||||||
|
// audit request/response entry which is serialized to JSON and stored within the event.
|
||||||
|
func (f *AuditFormatterJSON) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
const op = "event.(AuditFormatterJSON).Process"
|
||||||
|
if e == nil {
|
||||||
|
return nil, fmt.Errorf("%s: event is nil: %w", op, ErrInvalidParameter)
|
||||||
|
}
|
||||||
|
|
||||||
|
a, ok := e.Payload.(*audit)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%s: cannot parse event payload: %w", op, ErrInvalidParameter)
|
||||||
|
}
|
||||||
|
|
||||||
|
var formatted []byte
|
||||||
|
|
||||||
|
switch a.Subtype {
|
||||||
|
case AuditRequest:
|
||||||
|
entry, err := f.formatter.FormatRequest(ctx, f.config, a.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: unable to parse request from audit event: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted, err = jsonutil.EncodeJSON(entry)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: unable to format request: %w", op, err)
|
||||||
|
}
|
||||||
|
case AuditResponse:
|
||||||
|
entry, err := f.formatter.FormatResponse(ctx, f.config, a.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: unable to parse response from audit event: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted, err = jsonutil.EncodeJSON(entry)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: unable to format response: %w", op, err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%s: unknown audit event subtype: %q", op, a.Subtype)
|
||||||
|
}
|
||||||
|
|
||||||
|
e.FormattedAs(f.format.String(), formatted)
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
242
internal/observability/event/node_formatter_audit_json_test.go
Normal file
242
internal/observability/event/node_formatter_audit_json_test.go
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/helper/namespace"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
|
|
||||||
|
vaultaudit "github.com/hashicorp/vault/audit"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/sdk/helper/salt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/eventlogger"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fakeJSONAuditEvent will return a new fake event containing audit data based
|
||||||
|
// on the specified auditSubtype and logical.LogInput.
|
||||||
|
func fakeJSONAuditEvent(t *testing.T, subtype auditSubtype, input *logical.LogInput) *eventlogger.Event {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
date := time.Date(2023, time.July, 11, 15, 49, 10, 0o0, time.Local)
|
||||||
|
|
||||||
|
auditEvent, err := newAudit(
|
||||||
|
WithID("123"),
|
||||||
|
WithSubtype(string(subtype)),
|
||||||
|
WithFormat(string(AuditFormatJSON)),
|
||||||
|
WithNow(date),
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, auditEvent)
|
||||||
|
require.Equal(t, "123", auditEvent.ID)
|
||||||
|
require.Equal(t, "v0.1", auditEvent.Version)
|
||||||
|
require.Equal(t, AuditFormatJSON, auditEvent.RequiredFormat)
|
||||||
|
require.Equal(t, subtype, auditEvent.Subtype)
|
||||||
|
require.Equal(t, date, auditEvent.Timestamp)
|
||||||
|
|
||||||
|
auditEvent.Data = input
|
||||||
|
|
||||||
|
e := &eventlogger.Event{
|
||||||
|
Type: eventlogger.EventType(AuditType),
|
||||||
|
CreatedAt: auditEvent.Timestamp,
|
||||||
|
Formatted: make(map[string][]byte),
|
||||||
|
Payload: auditEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// newStaticSalt returns a new staticSalt for use in testing.
|
||||||
|
func newStaticSalt(t *testing.T) *staticSalt {
|
||||||
|
s, err := salt.NewSalt(context.Background(), nil, nil)
|
||||||
|
require.NoError(t, 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
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNewAuditFormatterJSON ensures we can create new AuditFormatterJSONX structs.
|
||||||
|
func TestNewAuditFormatterJSON(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
UseStaticSalt bool
|
||||||
|
IsErrorExpected bool
|
||||||
|
ExpectedErrorMessage string
|
||||||
|
}{
|
||||||
|
"nil-salter": {
|
||||||
|
UseStaticSalt: false,
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "event.NewAuditFormatterJSON: unable to create new JSON audit formatter: cannot create a new audit formatter with nil salter",
|
||||||
|
},
|
||||||
|
"static-salter": {
|
||||||
|
UseStaticSalt: true,
|
||||||
|
IsErrorExpected: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
cfg := vaultaudit.FormatterConfig{}
|
||||||
|
var ss vaultaudit.Salter
|
||||||
|
if tc.UseStaticSalt {
|
||||||
|
ss = newStaticSalt(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := NewAuditFormatterJSON(cfg, ss)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case tc.IsErrorExpected:
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
||||||
|
require.Nil(t, f)
|
||||||
|
default:
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, f)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAuditFormatterJSONX_Reopen ensures that we do no get an error when calling Reopen.
|
||||||
|
func TestAuditFormatterJSON_Reopen(t *testing.T) {
|
||||||
|
ss := newStaticSalt(t)
|
||||||
|
cfg := vaultaudit.FormatterConfig{}
|
||||||
|
|
||||||
|
f, err := NewAuditFormatterJSON(cfg, ss)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, f)
|
||||||
|
require.NoError(t, f.Reopen())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAuditFormatterJSONX_Type ensures that the node is a 'formatter' type.
|
||||||
|
func TestAuditFormatterJSON_Type(t *testing.T) {
|
||||||
|
ss := newStaticSalt(t)
|
||||||
|
cfg := vaultaudit.FormatterConfig{}
|
||||||
|
|
||||||
|
f, err := NewAuditFormatterJSON(cfg, ss)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, f)
|
||||||
|
require.Equal(t, eventlogger.NodeTypeFormatter, f.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestAuditFormatterJSON_Process attempts to run the Process method to convert
|
||||||
|
// the logical.LogInput within an audit event to JSON (AuditRequestEntry or AuditResponseEntry).
|
||||||
|
func TestAuditFormatterJSON_Process(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
IsErrorExpected bool
|
||||||
|
ExpectedErrorMessage string
|
||||||
|
Subtype auditSubtype
|
||||||
|
Data *logical.LogInput
|
||||||
|
RootNamespace bool
|
||||||
|
}{
|
||||||
|
"request-no-data": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "event.(AuditFormatterJSON).Process: unable to parse request from audit event: request to request-audit a nil request",
|
||||||
|
Subtype: AuditRequest,
|
||||||
|
Data: nil,
|
||||||
|
},
|
||||||
|
"response-no-data": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "event.(AuditFormatterJSON).Process: unable to parse response from audit event: request to response-audit a nil request",
|
||||||
|
Subtype: AuditResponse,
|
||||||
|
Data: nil,
|
||||||
|
},
|
||||||
|
"request-basic-input": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "event.(AuditFormatterJSON).Process: unable to parse request from audit event: request to request-audit a nil request",
|
||||||
|
Subtype: AuditRequest,
|
||||||
|
Data: &logical.LogInput{Type: "magic"},
|
||||||
|
},
|
||||||
|
"response-basic-input": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "event.(AuditFormatterJSON).Process: unable to parse response from audit event: request to response-audit a nil request",
|
||||||
|
Subtype: AuditResponse,
|
||||||
|
Data: &logical.LogInput{Type: "magic"},
|
||||||
|
},
|
||||||
|
"request-basic-input-and-request-no-ns": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "event.(AuditFormatterJSON).Process: unable to parse request from audit event: no namespace",
|
||||||
|
Subtype: AuditRequest,
|
||||||
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
||||||
|
},
|
||||||
|
"response-basic-input-and-request-no-ns": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "event.(AuditFormatterJSON).Process: unable to parse response from audit event: no namespace",
|
||||||
|
Subtype: AuditResponse,
|
||||||
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
||||||
|
},
|
||||||
|
"request-basic-input-and-request-with-ns": {
|
||||||
|
IsErrorExpected: false,
|
||||||
|
Subtype: AuditRequest,
|
||||||
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
||||||
|
RootNamespace: true,
|
||||||
|
},
|
||||||
|
"response-basic-input-and-request-with-ns": {
|
||||||
|
IsErrorExpected: false,
|
||||||
|
Subtype: AuditResponse,
|
||||||
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
||||||
|
RootNamespace: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
e := fakeJSONAuditEvent(t, tc.Subtype, tc.Data)
|
||||||
|
require.NotNil(t, e)
|
||||||
|
|
||||||
|
ss := newStaticSalt(t)
|
||||||
|
cfg := vaultaudit.FormatterConfig{}
|
||||||
|
|
||||||
|
f, err := NewAuditFormatterJSON(cfg, ss)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, f)
|
||||||
|
|
||||||
|
var ctx context.Context
|
||||||
|
switch {
|
||||||
|
case tc.RootNamespace:
|
||||||
|
ctx = namespace.RootContext(context.Background())
|
||||||
|
default:
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
processed, err := f.Process(ctx, e)
|
||||||
|
b, found := e.Format(string(AuditFormatJSON))
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case tc.IsErrorExpected:
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
||||||
|
require.Nil(t, processed)
|
||||||
|
require.False(t, found)
|
||||||
|
require.Nil(t, b)
|
||||||
|
default:
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, processed)
|
||||||
|
require.True(t, found)
|
||||||
|
require.NotNil(t, b)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -114,9 +114,9 @@ func TestOptions_WithNow(t *testing.T) {
|
|||||||
ExpectedErrorMessage: "cannot specify 'now' to be the zero time instant",
|
ExpectedErrorMessage: "cannot specify 'now' to be the zero time instant",
|
||||||
},
|
},
|
||||||
"valid-time": {
|
"valid-time": {
|
||||||
Value: time.Date(2023, time.July, 4, 12, 0o3, 0o0, 0o0, &time.Location{}),
|
Value: time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local),
|
||||||
IsErrorExpected: false,
|
IsErrorExpected: false,
|
||||||
ExpectedValue: time.Date(2023, time.July, 4, 12, 0o3, 0o0, 0o0, &time.Location{}),
|
ExpectedValue: time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -245,16 +245,16 @@ func TestOptions_Opts(t *testing.T) {
|
|||||||
},
|
},
|
||||||
"with-multiple-valid-now": {
|
"with-multiple-valid-now": {
|
||||||
opts: []Option{
|
opts: []Option{
|
||||||
WithNow(time.Date(2023, time.July, 4, 12, 0o3, 0o0, 0o0, &time.Location{})),
|
WithNow(time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local)),
|
||||||
WithNow(time.Date(2023, time.July, 4, 13, 0o3, 0o0, 0o0, &time.Location{})),
|
WithNow(time.Date(2023, time.July, 4, 13, 3, 0, 0, time.Local)),
|
||||||
},
|
},
|
||||||
IsErrorExpected: false,
|
IsErrorExpected: false,
|
||||||
ExpectedNow: time.Date(2023, time.July, 4, 13, 0o3, 0o0, 0o0, &time.Location{}),
|
ExpectedNow: time.Date(2023, time.July, 4, 13, 3, 0, 0, time.Local),
|
||||||
IsNowExpected: false,
|
IsNowExpected: false,
|
||||||
},
|
},
|
||||||
"with-multiple-valid-then-invalid-now": {
|
"with-multiple-valid-then-invalid-now": {
|
||||||
opts: []Option{
|
opts: []Option{
|
||||||
WithNow(time.Date(2023, time.July, 4, 12, 0o3, 0o0, 0o0, &time.Location{})),
|
WithNow(time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local)),
|
||||||
WithNow(time.Time{}),
|
WithNow(time.Time{}),
|
||||||
},
|
},
|
||||||
IsErrorExpected: true,
|
IsErrorExpected: true,
|
||||||
@@ -265,13 +265,13 @@ func TestOptions_Opts(t *testing.T) {
|
|||||||
WithID("qwerty"),
|
WithID("qwerty"),
|
||||||
WithSubtype("typey2"),
|
WithSubtype("typey2"),
|
||||||
WithFormat("json"),
|
WithFormat("json"),
|
||||||
WithNow(time.Date(2023, time.July, 4, 12, 0o3, 0o0, 0o0, &time.Location{})),
|
WithNow(time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local)),
|
||||||
},
|
},
|
||||||
IsErrorExpected: false,
|
IsErrorExpected: false,
|
||||||
ExpectedID: "qwerty",
|
ExpectedID: "qwerty",
|
||||||
ExpectedSubtype: "typey2",
|
ExpectedSubtype: "typey2",
|
||||||
ExpectedFormat: "json",
|
ExpectedFormat: "json",
|
||||||
ExpectedNow: time.Date(2023, time.July, 4, 12, 0o3, 0o0, 0o0, &time.Location{}),
|
ExpectedNow: time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local),
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user