mirror of https://github.com/hashicorp/boundary
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1280 lines
36 KiB
1280 lines
36 KiB
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,
|
|
})
|
|
}
|