* Have fallback eventing use json format The server will initialize a logger via InitFallbackLogger(...) after setting up the server's logging (and honoring the related cmd flags). This fallback logger will be used if when no eventer is initialized. If no fallback logger is initialized, then a event.defaultLogger() will be used if no eventer is defined. Co-authored-by: Jim <jlambert@hashicorp.com>
1280 lines
36 KiB
Go
1280 lines
36 KiB
Go
package event_test
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
stderrors "errors"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"os"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
pbs "github.com/hashicorp/boundary/internal/gen/testing/event"
|
|
"github.com/hashicorp/boundary/internal/observability/event"
|
|
"github.com/hashicorp/eventlogger/filters/encrypt"
|
|
"github.com/hashicorp/eventlogger/formatter_filters/cloudevents"
|
|
"github.com/hashicorp/go-hclog"
|
|
"github.com/mitchellh/copystructure"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"google.golang.org/protobuf/types/known/structpb"
|
|
)
|
|
|
|
const apiRequest = "APIRequest"
|
|
|
|
const (
|
|
testAuditVersion = "v0.1"
|
|
testObservationVersion = "v0.1"
|
|
)
|
|
|
|
type testAudit struct {
|
|
Id string `json:"id"` // std audit/boundary field
|
|
Version string `json:"version"` // std audit/boundary field
|
|
Type string `json:"type"` // std audit field
|
|
Timestamp time.Time `json:"timestamp"` // std audit field
|
|
RequestInfo *event.RequestInfo `json:"request_info,omitempty"` // boundary field
|
|
Auth *event.Auth `json:"auth,omitempty"` // std audit field
|
|
Request *event.Request `json:"request,omitempty"` // std audit field
|
|
Response *event.Response `json:"response,omitempty"` // std audit field
|
|
Flush bool `json:"-"`
|
|
}
|
|
|
|
func Test_NewRequestInfoContext(t *testing.T) {
|
|
testInfo := event.TestRequestInfo(t)
|
|
testInfoMissingId := event.TestRequestInfo(t)
|
|
testInfoMissingId.Id = ""
|
|
|
|
testInfoMissingEventId := event.TestRequestInfo(t)
|
|
testInfoMissingEventId.EventId = ""
|
|
|
|
tests := []struct {
|
|
name string
|
|
ctx context.Context
|
|
requestInfo *event.RequestInfo
|
|
wantErrIs error
|
|
wantErrContains string
|
|
}{
|
|
{
|
|
name: "missing-ctx",
|
|
requestInfo: testInfo,
|
|
wantErrIs: event.ErrInvalidParameter,
|
|
wantErrContains: "missing context",
|
|
},
|
|
{
|
|
name: "missing-request-info",
|
|
ctx: context.Background(),
|
|
wantErrIs: event.ErrInvalidParameter,
|
|
wantErrContains: "missing request info",
|
|
},
|
|
{
|
|
name: "missing-request-info-id",
|
|
ctx: context.Background(),
|
|
requestInfo: testInfoMissingId,
|
|
wantErrIs: event.ErrInvalidParameter,
|
|
wantErrContains: "missing request info id",
|
|
},
|
|
{
|
|
name: "missing-request-info-event-id",
|
|
ctx: context.Background(),
|
|
requestInfo: testInfoMissingEventId,
|
|
wantErrIs: event.ErrInvalidParameter,
|
|
wantErrContains: "missing request info event id",
|
|
},
|
|
{
|
|
name: "valid",
|
|
ctx: context.Background(),
|
|
requestInfo: testInfo,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
ctx, err := event.NewRequestInfoContext(tt.ctx, tt.requestInfo)
|
|
if tt.wantErrIs != nil {
|
|
require.Errorf(err, "should have gotten an error")
|
|
assert.Nilf(ctx, "context should be nil")
|
|
assert.ErrorIs(err, tt.wantErrIs)
|
|
if tt.wantErrContains != "" {
|
|
assert.Contains(err.Error(), tt.wantErrContains)
|
|
}
|
|
return
|
|
}
|
|
require.NoErrorf(err, "should not have been a problem getting the request info")
|
|
require.NotNilf(ctx, "cxt returned shouldn't be nil")
|
|
got, ok := event.RequestInfoFromContext(ctx)
|
|
require.Truef(ok, "should be ok to get the request info")
|
|
assert.Equal(tt.requestInfo, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_RequestInfoFromContext(t *testing.T) {
|
|
testInfo := event.TestRequestInfo(t)
|
|
|
|
testCtx, err := event.NewRequestInfoContext(context.Background(), testInfo)
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
ctx context.Context
|
|
wantInfo *event.RequestInfo
|
|
wantNotOk bool
|
|
}{
|
|
{
|
|
name: "missing-ctx",
|
|
wantNotOk: true,
|
|
},
|
|
{
|
|
name: "no-request-info",
|
|
ctx: context.Background(),
|
|
wantNotOk: true,
|
|
},
|
|
{
|
|
name: "valid",
|
|
ctx: testCtx,
|
|
wantInfo: testInfo,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
got, ok := event.RequestInfoFromContext(tt.ctx)
|
|
if tt.wantNotOk {
|
|
require.Falsef(ok, "should not have returned ok for the request info")
|
|
assert.Nilf(got, "should not have returned %q request info", got)
|
|
return
|
|
}
|
|
require.Truef(ok, "should have been okay for getting the request info")
|
|
require.NotNilf(got, "request info should not be nil")
|
|
assert.Equal(tt.wantInfo, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_NewEventerContext(t *testing.T) {
|
|
testSetup := event.TestEventerConfig(t, "Test_NewEventerContext")
|
|
testLock := &sync.Mutex{}
|
|
testLogger := testLogger(t, testLock)
|
|
testEventer, err := event.NewEventer(testLogger, testLock, "Test_NewEventerContext", testSetup.EventerConfig)
|
|
require.NoError(t, err)
|
|
tests := []struct {
|
|
name string
|
|
ctx context.Context
|
|
eventer *event.Eventer
|
|
wantErrIs error
|
|
wantErrContains string
|
|
}{
|
|
{
|
|
name: "missing-ctx",
|
|
eventer: testEventer,
|
|
wantErrIs: event.ErrInvalidParameter,
|
|
wantErrContains: "missing context",
|
|
},
|
|
{
|
|
name: "missing-eventer",
|
|
ctx: context.Background(),
|
|
wantErrIs: event.ErrInvalidParameter,
|
|
wantErrContains: "missing eventer",
|
|
},
|
|
{
|
|
name: "valid",
|
|
ctx: context.Background(),
|
|
eventer: testEventer,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
ctx, err := event.NewEventerContext(tt.ctx, tt.eventer)
|
|
if tt.wantErrIs != nil {
|
|
require.Errorf(err, "should have gotten an error")
|
|
assert.Nilf(ctx, "context should be nil")
|
|
assert.ErrorIs(err, tt.wantErrIs)
|
|
if tt.wantErrContains != "" {
|
|
assert.Contains(err.Error(), tt.wantErrContains)
|
|
}
|
|
return
|
|
}
|
|
require.NoErrorf(err, "should not have been a problem getting the eventer")
|
|
require.NotNilf(ctx, "cxt returned shouldn't be nil")
|
|
got, ok := event.EventerFromContext(ctx)
|
|
require.Truef(ok, "should be ok to get the eventer")
|
|
assert.Equal(tt.eventer, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_EventerFromContext(t *testing.T) {
|
|
testSetup := event.TestEventerConfig(t, "Test_EventerFromContext")
|
|
|
|
testLock := &sync.Mutex{}
|
|
testLogger := testLogger(t, testLock)
|
|
testEventer, err := event.NewEventer(testLogger, testLock, "Test_EventerFromContext", testSetup.EventerConfig)
|
|
require.NoError(t, err)
|
|
|
|
testEventerCtx, err := event.NewEventerContext(context.Background(), testEventer)
|
|
require.NoError(t, err)
|
|
|
|
tests := []struct {
|
|
name string
|
|
ctx context.Context
|
|
wantEventer *event.Eventer
|
|
wantNotOk bool
|
|
}{
|
|
{
|
|
name: "missing-ctx",
|
|
wantNotOk: true,
|
|
},
|
|
{
|
|
name: "no-eventer",
|
|
ctx: context.Background(),
|
|
wantNotOk: true,
|
|
},
|
|
{
|
|
name: "valid",
|
|
ctx: testEventerCtx,
|
|
wantEventer: testEventer,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
got, ok := event.EventerFromContext(tt.ctx)
|
|
if tt.wantNotOk {
|
|
require.Falsef(ok, "should not have returned ok for the eventer")
|
|
assert.Nilf(got, "should not have returned %q eventer", got)
|
|
return
|
|
}
|
|
require.Truef(ok, "should have been okay for getting an eventer")
|
|
require.NotNilf(got, "eventer should not be nil")
|
|
assert.Equal(tt.wantEventer, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_WriteObservation(t *testing.T) {
|
|
// this test and its subtests cannot be run in parallel because of it's
|
|
// dependency on the sysEventer
|
|
|
|
c := event.TestEventerConfig(t, "WriteObservation")
|
|
|
|
testLock := &sync.Mutex{}
|
|
testLogger := testLogger(t, testLock)
|
|
e, err := event.NewEventer(testLogger, testLock, "Test_WriteObservation", c.EventerConfig)
|
|
require.NoError(t, err)
|
|
|
|
info := &event.RequestInfo{Id: "867-5309", EventId: "411"}
|
|
|
|
testCtx, err := event.NewEventerContext(context.Background(), e)
|
|
require.NoError(t, err)
|
|
testCtx, err = event.NewRequestInfoContext(testCtx, info)
|
|
require.NoError(t, err)
|
|
|
|
testCtxNoEventInfoId, err := event.NewEventerContext(context.Background(), e)
|
|
require.NoError(t, err)
|
|
noEventId := &event.RequestInfo{Id: "867-5309", EventId: "411"}
|
|
testCtxNoEventInfoId, err = event.NewRequestInfoContext(testCtxNoEventInfoId, noEventId)
|
|
require.NoError(t, err)
|
|
noEventId.EventId = ""
|
|
noEventId.Id = ""
|
|
|
|
type observationPayload struct {
|
|
header []interface{}
|
|
details []interface{}
|
|
}
|
|
|
|
testPayloads := []observationPayload{
|
|
{
|
|
header: []interface{}{"name", "bar"},
|
|
},
|
|
{
|
|
header: []interface{}{"list", []string{"1", "2"}},
|
|
},
|
|
{
|
|
details: []interface{}{"file", "temp-file.txt"},
|
|
},
|
|
}
|
|
|
|
testWantHeader := map[string]interface{}{
|
|
"name": "bar",
|
|
"list": []string{"1", "2"},
|
|
}
|
|
|
|
testWantDetails := map[string]interface{}{
|
|
"file": "temp-file.txt",
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
noOperation bool
|
|
noFlush bool
|
|
observationPayload []observationPayload
|
|
header map[string]interface{}
|
|
details map[string]interface{}
|
|
ctx context.Context
|
|
observationSinkFileName string
|
|
setup func() error
|
|
cleanup func()
|
|
wantErrIs error
|
|
wantErrContains string
|
|
}{
|
|
{
|
|
name: "no-info-event-id",
|
|
noFlush: true,
|
|
ctx: testCtxNoEventInfoId,
|
|
observationPayload: []observationPayload{
|
|
{
|
|
header: []interface{}{"name", "bar"},
|
|
},
|
|
},
|
|
header: map[string]interface{}{
|
|
"name": "bar",
|
|
},
|
|
observationSinkFileName: c.AllEvents.Name(),
|
|
setup: func() error {
|
|
return event.InitSysEventer(testLogger, testLock, "no-info-event-id", event.WithEventerConfig(&c.EventerConfig))
|
|
},
|
|
cleanup: func() { event.TestResetSystEventer(t) },
|
|
},
|
|
{
|
|
name: "missing-ctx",
|
|
observationPayload: testPayloads,
|
|
wantErrIs: event.ErrInvalidParameter,
|
|
wantErrContains: "missing context",
|
|
},
|
|
{
|
|
name: "missing-op",
|
|
ctx: testCtx,
|
|
noOperation: true,
|
|
observationPayload: testPayloads,
|
|
wantErrIs: event.ErrInvalidParameter,
|
|
wantErrContains: "missing operation",
|
|
},
|
|
{
|
|
name: "no-header-or-details-in-payload",
|
|
noFlush: true,
|
|
ctx: testCtx,
|
|
observationPayload: []observationPayload{
|
|
{},
|
|
},
|
|
wantErrIs: event.ErrInvalidParameter,
|
|
wantErrContains: "specify either header or details options",
|
|
},
|
|
{
|
|
name: "no-ctx-eventer-and-syseventer-not-initialized",
|
|
ctx: context.Background(),
|
|
observationPayload: testPayloads,
|
|
wantErrIs: event.ErrInvalidParameter,
|
|
wantErrContains: "missing both context and system eventer",
|
|
},
|
|
{
|
|
name: "use-syseventer",
|
|
noFlush: true,
|
|
ctx: context.Background(),
|
|
observationPayload: []observationPayload{
|
|
{
|
|
header: []interface{}{"name", "bar"},
|
|
},
|
|
},
|
|
header: map[string]interface{}{
|
|
"name": "bar",
|
|
},
|
|
observationSinkFileName: c.AllEvents.Name(),
|
|
setup: func() error {
|
|
return event.InitSysEventer(testLogger, testLock, "use-syseventer", event.WithEventerConfig(&c.EventerConfig))
|
|
},
|
|
cleanup: func() { event.TestResetSystEventer(t) },
|
|
},
|
|
{
|
|
name: "use-syseventer-with-cancelled-ctx",
|
|
noFlush: true,
|
|
ctx: func() context.Context {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
return ctx
|
|
}(),
|
|
observationPayload: []observationPayload{
|
|
{
|
|
header: []interface{}{"name", "bar"},
|
|
},
|
|
},
|
|
header: map[string]interface{}{
|
|
"name": "bar",
|
|
},
|
|
observationSinkFileName: c.AllEvents.Name(),
|
|
setup: func() error {
|
|
return event.InitSysEventer(testLogger, testLock, "use-syseventer", event.WithEventerConfig(&c.EventerConfig))
|
|
},
|
|
cleanup: func() { event.TestResetSystEventer(t) },
|
|
},
|
|
{
|
|
name: "simple",
|
|
ctx: testCtx,
|
|
observationPayload: testPayloads,
|
|
header: testWantHeader,
|
|
details: testWantDetails,
|
|
observationSinkFileName: c.AllEvents.Name(),
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
if tt.setup != nil {
|
|
require.NoError(tt.setup())
|
|
}
|
|
if tt.cleanup != nil {
|
|
defer tt.cleanup()
|
|
}
|
|
op := tt.name
|
|
if tt.noOperation {
|
|
op = ""
|
|
}
|
|
require.Greater(len(tt.observationPayload), 0)
|
|
for _, p := range tt.observationPayload {
|
|
err := event.WriteObservation(tt.ctx, event.Op(op), event.WithHeader(p.header...), event.WithDetails(p.details...))
|
|
if tt.wantErrIs != nil {
|
|
assert.ErrorIs(err, tt.wantErrIs)
|
|
if tt.wantErrContains != "" {
|
|
assert.Contains(err.Error(), tt.wantErrContains)
|
|
}
|
|
return
|
|
}
|
|
require.NoError(err)
|
|
}
|
|
if !tt.noFlush {
|
|
require.NoError(event.WriteObservation(tt.ctx, event.Op(tt.name), event.WithFlush()))
|
|
}
|
|
|
|
if tt.observationSinkFileName != "" {
|
|
defer func() { _ = os.WriteFile(tt.observationSinkFileName, nil, 0o666) }()
|
|
b, err := ioutil.ReadFile(tt.observationSinkFileName)
|
|
assert.NoError(err)
|
|
|
|
gotObservation := &cloudevents.Event{}
|
|
err = json.Unmarshal(b, gotObservation)
|
|
require.NoErrorf(err, "json: %s", string(b))
|
|
|
|
actualJson, err := json.Marshal(gotObservation)
|
|
require.NoError(err)
|
|
wantJson := testObservationJsonFromCtx(t, tt.ctx, event.Op(tt.name), gotObservation, tt.header, tt.details)
|
|
|
|
assert.JSONEq(string(wantJson), string(actualJson))
|
|
}
|
|
})
|
|
}
|
|
t.Run("not-enabled", func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
|
|
c := event.TestEventerConfig(t, "WriteObservation")
|
|
c.EventerConfig.ObservationsEnabled = false
|
|
testLock := &sync.Mutex{}
|
|
e, err := event.NewEventer(testLogger, testLock, "not-enabled", c.EventerConfig)
|
|
require.NoError(err)
|
|
|
|
testCtx, err := event.NewEventerContext(context.Background(), e)
|
|
require.NoError(err)
|
|
testCtx, err = event.NewRequestInfoContext(testCtx, info)
|
|
require.NoError(err)
|
|
|
|
hdr := map[string]interface{}{
|
|
"list": []string{"1", "2"},
|
|
}
|
|
require.NoError(event.WriteObservation(testCtx, "not-enabled", event.WithHeader(hdr), event.WithFlush()))
|
|
|
|
b, err := ioutil.ReadFile(c.AllEvents.Name())
|
|
assert.NoError(err)
|
|
assert.Len(b, 0)
|
|
})
|
|
}
|
|
|
|
func Test_Filtering(t *testing.T) {
|
|
testLock := &sync.Mutex{}
|
|
testLogger := testLogger(t, testLock)
|
|
tests := []struct {
|
|
name string
|
|
allow []string
|
|
deny []string
|
|
hdr []interface{}
|
|
found bool
|
|
}{
|
|
{
|
|
name: "allowed",
|
|
allow: []string{`"/data/list" contains "1"`},
|
|
hdr: []interface{}{"list", []string{"1", "2"}},
|
|
found: true,
|
|
},
|
|
{
|
|
name: "not-allowed",
|
|
allow: []string{`"/data/list" contains "22"`},
|
|
hdr: []interface{}{"list", []string{"1", "2"}},
|
|
found: false,
|
|
},
|
|
{
|
|
name: "deny",
|
|
deny: []string{`"/data/list" contains "1"`},
|
|
hdr: []interface{}{"list", []string{"1", "2"}},
|
|
found: false,
|
|
},
|
|
{
|
|
name: "not-deny",
|
|
deny: []string{`"/data/list" contains "22"`},
|
|
hdr: []interface{}{"list", []string{"1", "2"}},
|
|
found: true,
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
info := &event.RequestInfo{Id: "867-5309", EventId: "411"}
|
|
|
|
c := event.TestEventerConfig(t, "WriteObservation-filtering")
|
|
c.EventerConfig.Sinks[0].AllowFilters = tt.allow
|
|
c.EventerConfig.Sinks[0].DenyFilters = tt.deny
|
|
|
|
e, err := event.NewEventer(testLogger, testLock, "filtering", c.EventerConfig)
|
|
require.NoError(err)
|
|
|
|
testCtx, err := event.NewEventerContext(context.Background(), e)
|
|
require.NoError(err)
|
|
testCtx, err = event.NewRequestInfoContext(testCtx, info)
|
|
require.NoError(err)
|
|
|
|
require.NoError(event.WriteObservation(testCtx, "not-enabled", event.WithHeader(tt.hdr...), event.WithFlush()))
|
|
|
|
b, err := ioutil.ReadFile(c.AllEvents.Name())
|
|
assert.NoError(err)
|
|
switch tt.found {
|
|
case true:
|
|
assert.NotEmpty(b)
|
|
case false:
|
|
assert.Empty(b)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func Test_DefaultEventerConfig(t *testing.T) {
|
|
t.Run("assert-default", func(t *testing.T) {
|
|
assert.Equal(t, &event.EventerConfig{
|
|
AuditEnabled: false,
|
|
ObservationsEnabled: true,
|
|
SysEventsEnabled: true,
|
|
Sinks: []*event.SinkConfig{event.DefaultSink()},
|
|
}, event.DefaultEventerConfig())
|
|
})
|
|
}
|
|
|
|
func testObservationJsonFromCtx(t *testing.T, ctx context.Context, caller event.Op, got *cloudevents.Event, hdr, details map[string]interface{}) []byte {
|
|
t.Helper()
|
|
require := require.New(t)
|
|
|
|
reqInfo, _ := event.RequestInfoFromContext(ctx)
|
|
// require.Truef(ok, "missing reqInfo in ctx")
|
|
|
|
j := cloudevents.Event{
|
|
ID: got.ID,
|
|
Time: got.Time,
|
|
Source: got.Source,
|
|
SpecVersion: got.SpecVersion,
|
|
Type: got.Type,
|
|
DataContentType: got.DataContentType,
|
|
Data: map[string]interface{}{
|
|
event.RequestInfoField: reqInfo,
|
|
event.VersionField: testObservationVersion,
|
|
},
|
|
}
|
|
if hdr != nil {
|
|
h := j.Data.(map[string]interface{})
|
|
for k, v := range hdr {
|
|
h[k] = v
|
|
}
|
|
}
|
|
if details != nil {
|
|
details[event.OpField] = string(caller)
|
|
d := got.Data.(map[string]interface{})[event.DetailsField].([]interface{})[0].(map[string]interface{})
|
|
j.Data.(map[string]interface{})[event.DetailsField] = []struct {
|
|
CreatedAt string `json:"created_at"`
|
|
Type string `json:"type"`
|
|
Payload map[string]interface{} `json:"payload"`
|
|
}{
|
|
{
|
|
CreatedAt: d[event.CreatedAtField].(string),
|
|
Type: d[event.TypeField].(string),
|
|
Payload: details,
|
|
},
|
|
}
|
|
}
|
|
b, err := json.Marshal(j)
|
|
require.NoError(err)
|
|
return b
|
|
}
|
|
|
|
func Test_WriteAudit(t *testing.T) {
|
|
// this test and its subtests cannot be run in parallel because of it's
|
|
// dependency on the sysEventer
|
|
now := time.Now()
|
|
|
|
c := event.TestEventerConfig(t, "WriteAudit")
|
|
testLock := &sync.Mutex{}
|
|
testLogger := testLogger(t, testLock)
|
|
e, err := event.NewEventer(testLogger, testLock, "Test_WriteAudit", c.EventerConfig)
|
|
require.NoError(t, err)
|
|
|
|
info := &event.RequestInfo{Id: "867-5309", EventId: "411"}
|
|
|
|
ctx, err := event.NewEventerContext(context.Background(), e)
|
|
require.NoError(t, err)
|
|
ctx, err = event.NewRequestInfoContext(ctx, info)
|
|
require.NoError(t, err)
|
|
|
|
testAuth := &event.Auth{
|
|
AuthTokenId: "test_auth_token_id",
|
|
UserEmail: "test_user_email",
|
|
UserName: "test_user_name",
|
|
UserInfo: &event.UserInfo{
|
|
UserId: "test_user_id",
|
|
AuthAccountId: "test_auth_account_id",
|
|
},
|
|
GrantsInfo: &event.GrantsInfo{
|
|
Grants: []event.Grant{
|
|
{
|
|
Grant: "test_grant",
|
|
ScopeId: "test_grant_scope_id",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
testReq := &event.Request{
|
|
Operation: "POST",
|
|
Endpoint: "/v1/hosts",
|
|
Details: &pbs.TestAuthenticateRequest{
|
|
AuthMethodId: "test_1234567890",
|
|
TokenType: "test-cookie",
|
|
Command: "test-command",
|
|
Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{
|
|
"password": structpb.NewStringValue("fido"),
|
|
}},
|
|
},
|
|
}
|
|
|
|
testResp := &event.Response{
|
|
StatusCode: 200,
|
|
Details: &pbs.TestAuthenticateResponse{
|
|
Command: "test-command",
|
|
Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{
|
|
"token": structpb.NewStringValue("test-token"),
|
|
}},
|
|
},
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
auditOpts [][]event.Option
|
|
wantAudit *testAudit
|
|
ctx context.Context
|
|
auditSinkFileName string
|
|
setup func() error
|
|
cleanup func()
|
|
noOperation bool
|
|
noFlush bool
|
|
wantErrIs error
|
|
wantErrContains string
|
|
}{
|
|
{
|
|
name: "missing-ctx",
|
|
auditOpts: [][]event.Option{
|
|
{
|
|
event.WithAuth(testAuth),
|
|
event.WithRequest(testReq),
|
|
},
|
|
},
|
|
wantErrIs: event.ErrInvalidParameter,
|
|
wantErrContains: "missing context",
|
|
},
|
|
{
|
|
name: "missing-op",
|
|
ctx: ctx,
|
|
auditOpts: [][]event.Option{
|
|
{
|
|
event.WithAuth(testAuth),
|
|
event.WithRequest(testReq),
|
|
},
|
|
},
|
|
noOperation: true,
|
|
wantErrIs: event.ErrInvalidParameter,
|
|
wantErrContains: "missing operation",
|
|
},
|
|
{
|
|
name: "no-ctx-eventer-and-syseventer-not-initialized",
|
|
ctx: context.Background(),
|
|
auditOpts: [][]event.Option{
|
|
{
|
|
event.WithAuth(testAuth),
|
|
event.WithRequest(testReq),
|
|
},
|
|
},
|
|
wantErrIs: event.ErrInvalidParameter,
|
|
wantErrContains: "missing both context and system eventer",
|
|
},
|
|
{
|
|
name: "use-syseventer",
|
|
noFlush: true,
|
|
ctx: context.Background(),
|
|
auditOpts: [][]event.Option{
|
|
{
|
|
event.WithAuth(
|
|
func() *event.Auth {
|
|
dup, err := copystructure.Copy(testAuth)
|
|
require.NoError(t, err)
|
|
return dup.(*event.Auth)
|
|
}(),
|
|
),
|
|
event.WithRequest(
|
|
func() *event.Request {
|
|
dup, err := copystructure.Copy(testReq)
|
|
require.NoError(t, err)
|
|
return dup.(*event.Request)
|
|
}(),
|
|
),
|
|
},
|
|
},
|
|
wantAudit: &testAudit{
|
|
Auth: func() *event.Auth {
|
|
dup, err := copystructure.Copy(testAuth)
|
|
require.NoError(t, err)
|
|
dup.(*event.Auth).UserEmail = encrypt.RedactedData
|
|
dup.(*event.Auth).UserName = encrypt.RedactedData
|
|
return dup.(*event.Auth)
|
|
}(),
|
|
Request: func() *event.Request {
|
|
dup, err := copystructure.Copy(testReq)
|
|
require.NoError(t, err)
|
|
dup.(*event.Request).Details.(*pbs.TestAuthenticateRequest).Attributes = &structpb.Struct{
|
|
Fields: map[string]*structpb.Value{
|
|
"password": structpb.NewStringValue(encrypt.RedactedData),
|
|
},
|
|
}
|
|
return dup.(*event.Request)
|
|
}(),
|
|
},
|
|
setup: func() error {
|
|
return event.InitSysEventer(testLogger, testLock, "use-syseventer", event.WithEventerConfig(&c.EventerConfig))
|
|
},
|
|
cleanup: func() { event.TestResetSystEventer(t) },
|
|
auditSinkFileName: c.AllEvents.Name(),
|
|
},
|
|
{
|
|
name: "use-syseventer-with-cancelled-ctx",
|
|
noFlush: true,
|
|
ctx: func() context.Context {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
return ctx
|
|
}(),
|
|
auditOpts: [][]event.Option{
|
|
{
|
|
event.WithAuth(testAuth),
|
|
event.WithRequest(testReq),
|
|
},
|
|
},
|
|
wantAudit: &testAudit{
|
|
Auth: func() *event.Auth {
|
|
dup, err := copystructure.Copy(testAuth)
|
|
require.NoError(t, err)
|
|
dup.(*event.Auth).UserEmail = encrypt.RedactedData
|
|
dup.(*event.Auth).UserName = encrypt.RedactedData
|
|
return dup.(*event.Auth)
|
|
}(),
|
|
Request: func() *event.Request {
|
|
dup, err := copystructure.Copy(testReq)
|
|
require.NoError(t, err)
|
|
dup.(*event.Request).Details.(*pbs.TestAuthenticateRequest).Attributes = &structpb.Struct{
|
|
Fields: map[string]*structpb.Value{
|
|
"password": structpb.NewStringValue(encrypt.RedactedData),
|
|
},
|
|
}
|
|
return dup.(*event.Request)
|
|
}(),
|
|
},
|
|
setup: func() error {
|
|
return event.InitSysEventer(testLogger, testLock, "use-syseventer", event.WithEventerConfig(&c.EventerConfig))
|
|
},
|
|
cleanup: func() { event.TestResetSystEventer(t) },
|
|
auditSinkFileName: c.AllEvents.Name(),
|
|
},
|
|
{
|
|
name: "simple",
|
|
ctx: ctx,
|
|
auditOpts: [][]event.Option{
|
|
{
|
|
event.WithAuth(testAuth),
|
|
event.WithRequest(testReq),
|
|
},
|
|
{
|
|
event.WithResponse(testResp),
|
|
},
|
|
},
|
|
wantAudit: &testAudit{
|
|
Id: "411",
|
|
Auth: func() *event.Auth {
|
|
dup, err := copystructure.Copy(testAuth)
|
|
require.NoError(t, err)
|
|
dup.(*event.Auth).UserEmail = encrypt.RedactedData
|
|
dup.(*event.Auth).UserName = encrypt.RedactedData
|
|
return dup.(*event.Auth)
|
|
}(),
|
|
Request: func() *event.Request {
|
|
dup, err := copystructure.Copy(testReq)
|
|
require.NoError(t, err)
|
|
dup.(*event.Request).Details.(*pbs.TestAuthenticateRequest).Attributes = &structpb.Struct{
|
|
Fields: map[string]*structpb.Value{
|
|
"password": structpb.NewStringValue(encrypt.RedactedData),
|
|
},
|
|
}
|
|
return dup.(*event.Request)
|
|
}(),
|
|
Response: func() *event.Response {
|
|
dup, err := copystructure.Copy(testResp)
|
|
require.NoError(t, err)
|
|
dup.(*event.Response).Details.(*pbs.TestAuthenticateResponse).Attributes = &structpb.Struct{
|
|
Fields: map[string]*structpb.Value{
|
|
"token": structpb.NewStringValue(encrypt.RedactedData),
|
|
},
|
|
}
|
|
return dup.(*event.Response)
|
|
}(),
|
|
},
|
|
auditSinkFileName: c.AllEvents.Name(),
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
if tt.setup != nil {
|
|
require.NoError(tt.setup())
|
|
}
|
|
if tt.cleanup != nil {
|
|
defer tt.cleanup()
|
|
}
|
|
op := tt.name
|
|
if tt.noOperation {
|
|
op = ""
|
|
}
|
|
require.Greater(len(tt.auditOpts), 0)
|
|
for _, opts := range tt.auditOpts {
|
|
opts := append(opts, event.WithNow(now))
|
|
err := event.WriteAudit(tt.ctx, event.Op(op), opts...)
|
|
if tt.wantErrIs != nil {
|
|
assert.ErrorIs(err, tt.wantErrIs)
|
|
if tt.wantErrContains != "" {
|
|
assert.Contains(err.Error(), tt.wantErrContains)
|
|
}
|
|
return
|
|
}
|
|
require.NoError(err)
|
|
}
|
|
if !tt.noFlush {
|
|
require.NoError(event.WriteAudit(tt.ctx, event.Op(op), event.WithFlush(), event.WithNow(now)))
|
|
}
|
|
if tt.auditSinkFileName != "" {
|
|
defer func() { _ = os.WriteFile(tt.auditSinkFileName, nil, 0o666) }()
|
|
|
|
b, err := ioutil.ReadFile(tt.auditSinkFileName)
|
|
require.NoError(err)
|
|
gotAudit := &cloudevents.Event{}
|
|
err = json.Unmarshal(b, gotAudit)
|
|
require.NoErrorf(err, "json: %s", string(b))
|
|
|
|
actualJson, err := json.Marshal(gotAudit)
|
|
require.NoError(err)
|
|
wantEvent := cloudevents.Event{
|
|
ID: gotAudit.ID,
|
|
Source: gotAudit.Source,
|
|
SpecVersion: gotAudit.SpecVersion,
|
|
DataContentType: gotAudit.DataContentType,
|
|
Time: gotAudit.Time,
|
|
Type: "audit",
|
|
Data: map[string]interface{}{
|
|
"auth": tt.wantAudit.Auth,
|
|
"id": gotAudit.Data.(map[string]interface{})["id"],
|
|
"timestamp": now,
|
|
"request": tt.wantAudit.Request,
|
|
"type": apiRequest,
|
|
"version": testAuditVersion,
|
|
},
|
|
}
|
|
if tt.wantAudit.Id != "" {
|
|
wantEvent.Data.(map[string]interface{})["id"] = tt.wantAudit.Id
|
|
wantEvent.Data.(map[string]interface{})["request_info"] = event.RequestInfo{
|
|
Id: gotAudit.Data.(map[string]interface{})["request_info"].(map[string]interface{})["id"].(string),
|
|
}
|
|
}
|
|
if tt.wantAudit.Response != nil {
|
|
wantEvent.Data.(map[string]interface{})["response"] = tt.wantAudit.Response
|
|
}
|
|
wantJson, err := json.Marshal(wantEvent)
|
|
require.NoError(err)
|
|
|
|
assert.JSONEq(string(wantJson), string(actualJson))
|
|
}
|
|
})
|
|
}
|
|
t.Run("not-enabled", func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
c := event.TestEventerConfig(t, "WriteAudit")
|
|
c.EventerConfig.AuditEnabled = false
|
|
testLock := &sync.Mutex{}
|
|
e, err := event.NewEventer(testLogger, testLock, "not-enabled", c.EventerConfig)
|
|
require.NoError(err)
|
|
|
|
testCtx, err := event.NewEventerContext(context.Background(), e)
|
|
require.NoError(err)
|
|
testCtx, err = event.NewRequestInfoContext(testCtx, info)
|
|
require.NoError(err)
|
|
|
|
require.NoError(event.WriteAudit(testCtx, "not-enabled", event.WithRequest(testReq), event.WithFlush()))
|
|
b, err := ioutil.ReadFile(c.AllEvents.Name())
|
|
assert.NoError(err)
|
|
assert.Len(b, 0)
|
|
})
|
|
}
|
|
|
|
func Test_WriteError(t *testing.T) {
|
|
// this test and its subtests cannot be run in parallel because of it's
|
|
// dependency on the sysEventer
|
|
now := time.Now()
|
|
|
|
c := event.TestEventerConfig(t, "WriteAudit")
|
|
testLock := &sync.Mutex{}
|
|
testLogger := testLogger(t, testLock)
|
|
e, err := event.NewEventer(testLogger, testLock, "Test_WriteError", c.EventerConfig, event.WithNow(now))
|
|
require.NoError(t, err)
|
|
|
|
info := &event.RequestInfo{Id: "867-5309", EventId: "411"}
|
|
|
|
testCtx, err := event.NewEventerContext(context.Background(), e)
|
|
require.NoError(t, err)
|
|
testCtx, err = event.NewRequestInfoContext(testCtx, info)
|
|
require.NoError(t, err)
|
|
|
|
testCtxNoInfoId, err := event.NewEventerContext(context.Background(), e)
|
|
require.NoError(t, err)
|
|
noId := &event.RequestInfo{Id: "867-5309", EventId: "411"}
|
|
testCtxNoInfoId, err = event.NewRequestInfoContext(testCtxNoInfoId, noId)
|
|
require.NoError(t, err)
|
|
noId.Id = ""
|
|
noId.EventId = ""
|
|
|
|
testError := fakeError{
|
|
Msg: "test",
|
|
Code: "code",
|
|
}
|
|
|
|
tests := []struct {
|
|
name string
|
|
ctx context.Context
|
|
e error
|
|
opt []event.Option
|
|
info *event.RequestInfo
|
|
setup func() error
|
|
cleanup func()
|
|
noOperation bool
|
|
errSinkFileName string
|
|
noOutput bool
|
|
}{
|
|
{
|
|
name: "missing-caller",
|
|
ctx: testCtx,
|
|
e: &testError,
|
|
noOperation: true,
|
|
noOutput: true,
|
|
},
|
|
{
|
|
name: "no-ctx-eventer-and-syseventer-not-initialized",
|
|
ctx: context.Background(),
|
|
e: &testError,
|
|
errSinkFileName: c.ErrorEvents.Name(),
|
|
noOutput: true,
|
|
},
|
|
{
|
|
name: "use-syseventer",
|
|
ctx: context.Background(),
|
|
e: &testError,
|
|
setup: func() error {
|
|
return event.InitSysEventer(testLogger, testLock, "use-syseventer", event.WithEventerConfig(&c.EventerConfig))
|
|
},
|
|
cleanup: func() { event.TestResetSystEventer(t) },
|
|
errSinkFileName: c.ErrorEvents.Name(),
|
|
},
|
|
{
|
|
name: "use-syseventer-with-cancelled-ctx",
|
|
ctx: func() context.Context {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
return ctx
|
|
}(),
|
|
e: &testError,
|
|
setup: func() error {
|
|
return event.InitSysEventer(testLogger, testLock, "use-syseventer", event.WithEventerConfig(&c.EventerConfig))
|
|
},
|
|
cleanup: func() { event.TestResetSystEventer(t) },
|
|
errSinkFileName: c.ErrorEvents.Name(),
|
|
},
|
|
{
|
|
name: "no-info-id",
|
|
ctx: testCtxNoInfoId,
|
|
e: &testError,
|
|
info: &event.RequestInfo{},
|
|
setup: func() error {
|
|
return event.InitSysEventer(testLogger, testLock, "no-info-id", event.WithEventerConfig(&c.EventerConfig))
|
|
},
|
|
cleanup: func() { event.TestResetSystEventer(t) },
|
|
errSinkFileName: c.ErrorEvents.Name(),
|
|
},
|
|
{
|
|
name: "simple",
|
|
ctx: testCtx,
|
|
e: &testError,
|
|
info: info,
|
|
errSinkFileName: c.ErrorEvents.Name(),
|
|
},
|
|
{
|
|
name: "simple-with-opt",
|
|
ctx: testCtx,
|
|
e: &testError,
|
|
opt: []event.Option{event.WithInfo("test", "info")},
|
|
info: info,
|
|
errSinkFileName: c.ErrorEvents.Name(),
|
|
},
|
|
{
|
|
name: "stderrors",
|
|
ctx: testCtx,
|
|
e: stderrors.New("test std errors"),
|
|
opt: []event.Option{event.WithInfo("test", "info")},
|
|
info: info,
|
|
errSinkFileName: c.ErrorEvents.Name(),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
if tt.setup != nil {
|
|
require.NoError(tt.setup())
|
|
}
|
|
if tt.cleanup != nil {
|
|
defer tt.cleanup()
|
|
}
|
|
op := tt.name
|
|
if tt.noOperation {
|
|
op = ""
|
|
}
|
|
event.WriteError(tt.ctx, event.Op(op), tt.e, tt.opt...)
|
|
if tt.errSinkFileName != "" {
|
|
defer func() { _ = os.WriteFile(tt.errSinkFileName, nil, 0o666) }()
|
|
b, err := ioutil.ReadFile(tt.errSinkFileName)
|
|
require.NoError(err)
|
|
|
|
if tt.noOutput {
|
|
assert.Lenf(b, 0, "should be an empty file: %s", string(b))
|
|
return
|
|
}
|
|
|
|
gotError := &cloudevents.Event{}
|
|
err = json.Unmarshal(b, gotError)
|
|
require.NoErrorf(err, "json: %s", string(b))
|
|
|
|
if _, ok := gotError.Data.(map[string]interface{})["error_fields"].(map[string]interface{})["Msg"]; ok {
|
|
actualError := fakeError{
|
|
Msg: gotError.Data.(map[string]interface{})["error_fields"].(map[string]interface{})["Msg"].(string),
|
|
Code: gotError.Data.(map[string]interface{})["error_fields"].(map[string]interface{})["Code"].(string),
|
|
}
|
|
assert.Equal(tt.e, &actualError)
|
|
}
|
|
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type fakeError struct {
|
|
Code string
|
|
Msg string
|
|
}
|
|
|
|
func (f *fakeError) Error() string {
|
|
return f.Msg
|
|
}
|
|
|
|
func Test_WriteSysEvent(t *testing.T) {
|
|
// this test and its subtests cannot be run in parallel because of it's
|
|
// dependency on the sysEventer
|
|
|
|
ctx := context.Background()
|
|
c := event.TestEventerConfig(t, "Test_WriteSysEvent")
|
|
testLock := &sync.Mutex{}
|
|
testLogger := testLogger(t, testLock)
|
|
|
|
tests := []struct {
|
|
name string
|
|
ctx context.Context
|
|
data []interface{}
|
|
msg string
|
|
info *event.RequestInfo
|
|
setup func() error
|
|
cleanup func()
|
|
noOperation bool
|
|
sinkFileName string
|
|
noOutput bool
|
|
}{
|
|
{
|
|
name: "no-data",
|
|
ctx: ctx,
|
|
noOutput: true,
|
|
},
|
|
{
|
|
name: "missing-caller",
|
|
ctx: ctx,
|
|
msg: "hello",
|
|
data: []interface{}{"data", "test-data"},
|
|
noOperation: true,
|
|
},
|
|
{
|
|
name: "syseventer-not-initialized",
|
|
ctx: context.Background(),
|
|
msg: "hello",
|
|
data: []interface{}{"data", "test-data"},
|
|
sinkFileName: c.AllEvents.Name(),
|
|
noOutput: true,
|
|
},
|
|
{
|
|
name: "use-syseventer",
|
|
ctx: context.Background(),
|
|
msg: "hello",
|
|
data: []interface{}{"data", "test-data", event.ServerName, "test-server", event.ServerAddress, "localhost"},
|
|
setup: func() error {
|
|
return event.InitSysEventer(testLogger, testLock, "use-syseventer", event.WithEventerConfig(&c.EventerConfig))
|
|
},
|
|
cleanup: func() { event.TestResetSystEventer(t) },
|
|
sinkFileName: c.AllEvents.Name(),
|
|
},
|
|
{
|
|
name: "use-syseventer-with-cancelled-ctx",
|
|
ctx: func() context.Context {
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
defer cancel()
|
|
return ctx
|
|
}(),
|
|
msg: "hello",
|
|
data: []interface{}{"data", "test-data", event.ServerName, "test-server", event.ServerAddress, "localhost"},
|
|
setup: func() error {
|
|
return event.InitSysEventer(testLogger, testLock, "use-syseventer", event.WithEventerConfig(&c.EventerConfig))
|
|
},
|
|
cleanup: func() { event.TestResetSystEventer(t) },
|
|
sinkFileName: c.AllEvents.Name(),
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert, require := assert.New(t), require.New(t)
|
|
if tt.setup != nil {
|
|
require.NoError(tt.setup())
|
|
}
|
|
if tt.cleanup != nil {
|
|
defer tt.cleanup()
|
|
}
|
|
op := tt.name
|
|
if tt.noOperation {
|
|
op = ""
|
|
}
|
|
event.WriteSysEvent(tt.ctx, event.Op(op), tt.msg, tt.data...)
|
|
if tt.sinkFileName != "" {
|
|
defer func() { _ = os.WriteFile(tt.sinkFileName, nil, 0o666) }()
|
|
b, err := ioutil.ReadFile(tt.sinkFileName)
|
|
require.NoError(err)
|
|
|
|
if tt.noOutput {
|
|
assert.Lenf(b, 0, "should be an empty file: %s", string(b))
|
|
return
|
|
}
|
|
|
|
gotSysEvent := &cloudevents.Event{}
|
|
err = json.Unmarshal(b, gotSysEvent)
|
|
require.NoErrorf(err, "json: %s", string(b))
|
|
|
|
expected := event.ConvertArgs(tt.data...)
|
|
expected["msg"] = tt.msg
|
|
assert.Equal(expected, gotSysEvent.Data.(map[string]interface{})["data"].(map[string]interface{}))
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConvertArgs(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
args []interface{}
|
|
want map[string]interface{}
|
|
}{
|
|
{
|
|
name: "no-args",
|
|
args: []interface{}{},
|
|
want: nil,
|
|
},
|
|
{
|
|
name: "nil-first-arg",
|
|
args: []interface{}{nil},
|
|
want: map[string]interface{}{
|
|
event.MissingKey: nil,
|
|
},
|
|
},
|
|
{
|
|
name: "odd-number-of-args",
|
|
args: []interface{}{1, 2, 3},
|
|
want: map[string]interface{}{
|
|
"1": 2,
|
|
event.MissingKey: 3,
|
|
},
|
|
},
|
|
{
|
|
name: "struct-key",
|
|
args: []interface{}{[]struct{ name string }{{name: "alice"}}, 1},
|
|
want: map[string]interface{}{
|
|
"[{alice}]": 1,
|
|
},
|
|
},
|
|
{
|
|
name: "test-key-with-stringer",
|
|
args: []interface{}{testIntKeyWithStringer(11), "eleven"},
|
|
want: map[string]interface{}{
|
|
"*11*": "eleven",
|
|
},
|
|
},
|
|
}
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
assert := assert.New(t)
|
|
got := event.ConvertArgs(tt.args...)
|
|
assert.Equal(tt.want, got)
|
|
})
|
|
}
|
|
}
|
|
|
|
type testIntKeyWithStringer int
|
|
|
|
func (ti testIntKeyWithStringer) String() string {
|
|
return fmt.Sprint("*", int(ti), "*")
|
|
}
|
|
|
|
func testLogger(t *testing.T, testLock hclog.Locker) hclog.Logger {
|
|
t.Helper()
|
|
return hclog.New(&hclog.LoggerOptions{
|
|
Mutex: testLock,
|
|
Name: "test",
|
|
JSONFormat: true,
|
|
})
|
|
}
|