Merge remote-tracking branch 'origin/main' into ICU-1573

pull/1289/head^2
Jeff Mitchell 5 years ago
commit d6ef2732e7

@ -22,6 +22,7 @@ require (
github.com/hashicorp/cap v0.0.0-20210518163718-e72205e8eaae
github.com/hashicorp/dbassert v0.0.0-20200930125617-6218396928df
github.com/hashicorp/errwrap v1.1.0
github.com/hashicorp/eventlogger v0.0.0-20210523164657-c216620e1746
github.com/hashicorp/go-bexpr v0.1.8
github.com/hashicorp/go-cleanhttp v0.5.2
github.com/hashicorp/go-hclog v0.16.1

@ -311,8 +311,9 @@ github.com/go-sql-driver/mysql v1.5.0 h1:ozyZYNQW3x3HtqT1jira07DN2PArx2v7/mN66gG
github.com/go-sql-driver/mysql v1.5.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg=
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
github.com/go-test/deep v1.0.2-0.20181118220953-042da051cf31/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.2 h1:onZX1rnHT3Wv6cqNgYyFOOlgVKJrksuCMCRvJStbMYw=
github.com/go-test/deep v1.0.2/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/go-test/deep v1.0.4 h1:u2CU3YKy9I2pmu9pX0eq50wCgjfGIt539SqR7FbHiho=
github.com/go-test/deep v1.0.4/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
github.com/gobuffalo/here v0.6.0/go.mod h1:wAG085dHOYqUpf+Ap+WOdrPTp5IYcDAs/x7PLa8Y5fM=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0=
github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo=
@ -449,6 +450,8 @@ github.com/hashicorp/dbassert v0.0.0-20200930125617-6218396928df/go.mod h1:+B5eZ
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/eventlogger v0.0.0-20210523164657-c216620e1746 h1:uFLAYWBCufq4idhqjltAWb4s2JuJt1oE1hDRixwWqwY=
github.com/hashicorp/eventlogger v0.0.0-20210523164657-c216620e1746/go.mod h1:LG0lqGlYKC9FD5m5Umh7FW8SeJlciNUZv400J4+j094=
github.com/hashicorp/go-bexpr v0.1.8 h1:ETfuLF1bBAuHW/Qg6l1xCdV8WJ7lfatLtJ1N1w0IsfE=
github.com/hashicorp/go-bexpr v0.1.8/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0=
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
@ -984,6 +987,8 @@ go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.0.0 h1:qsup4IcBdlmsnGfqyLl4Ntn3C2XCCuKAE7DwHpScyUo=
go.uber.org/goleak v1.0.0/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4=
go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU=
@ -1232,6 +1237,7 @@ golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191112195655-aa38f8e97acc/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package accountscmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package accountscmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package accountscmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package authmethodscmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package authmethodscmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package authmethodscmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package authtokenscmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package groupscmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package hostcatalogscmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package hostcatalogscmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package hostscmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package hostscmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package hostsetscmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package hostsetscmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package rolescmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package scopescmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package sessionscmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package targetscmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package targetscmd
import (

@ -1,4 +1,4 @@
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package userscmd
import (

@ -77,7 +77,7 @@ var cmdTemplate = template.Must(template.New("").Funcs(
"hasAction": hasAction,
},
).Parse(`{{ $input := . }}
// Code generated by "make api"; DO NOT EDIT.
// Code generated by "make cli"; DO NOT EDIT.
package {{ .Pkg }}cmd
import (

@ -0,0 +1,231 @@
package event
import (
"context"
"fmt"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/go-hclog"
)
type key int
const (
eventerKey key = iota
requestInfoKey
)
// NewEventerContext will return a context containing a value of the provided Eventer
func NewEventerContext(ctx context.Context, eventer *Eventer) (context.Context, error) {
const op = "event.NewEventerContext"
if ctx == nil {
return nil, errors.New(errors.InvalidParameter, op, "missing context")
}
if eventer == nil {
return nil, errors.New(errors.InvalidParameter, op, "missing eventer")
}
return context.WithValue(ctx, eventerKey, eventer), nil
}
// EventerFromContext attempts to get the eventer value from the context provided
func EventerFromContext(ctx context.Context) (*Eventer, bool) {
if ctx == nil {
return nil, false
}
eventer, ok := ctx.Value(eventerKey).(*Eventer)
return eventer, ok
}
// NewRequestInfoContext will return a context containing a value for the
// provided RequestInfo
func NewRequestInfoContext(ctx context.Context, info *RequestInfo) (context.Context, error) {
const op = "event.NewRequestInfoContext"
if ctx == nil {
return nil, errors.New(errors.InvalidParameter, op, "missing context")
}
if info == nil {
return nil, errors.New(errors.InvalidParameter, op, "missing request info")
}
if info.Id == "" {
return nil, errors.New(errors.InvalidParameter, op, "missing request info id")
}
return context.WithValue(ctx, requestInfoKey, info), nil
}
// RequestInfoFromContext attempts to get the RequestInfo value from the context
// provided
func RequestInfoFromContext(ctx context.Context) (*RequestInfo, bool) {
if ctx == nil {
return nil, false
}
reqInfo, ok := ctx.Value(requestInfoKey).(*RequestInfo)
return reqInfo, ok
}
// WriteObservation will write an observation event. It will first check the
// ctx for an eventer, then try event.SysEventer() and if no eventer can be
// found an error is returned.
//
// At least one and any combination of the supported options may be used:
// WithHeader, WithDetails, WithId, WithFlush and WithRequestInfo. All other
// options are ignored.
func WriteObservation(ctx context.Context, caller Op, opt ...Option) error {
const op = "event.WriteObservation"
if ctx == nil {
return errors.New(errors.InvalidParameter, op, "missing context")
}
if caller == "" {
return errors.New(errors.InvalidParameter, op, "missing operation")
}
eventer, ok := EventerFromContext(ctx)
if !ok {
eventer = SysEventer()
if eventer == nil {
return errors.New(errors.InvalidParameter, op, "missing both context and system eventer")
}
}
opts := getOpts(opt...)
if opts.withDetails == nil && opts.withHeader == nil && !opts.withFlush {
return errors.New(errors.InvalidParameter, op, "you must specify either header or details options for an event payload")
}
if opts.withRequestInfo == nil {
var err error
if opt, err = addCtxOptions(ctx, opt...); err != nil {
return errors.Wrap(err, op)
}
}
e, err := newObservation(caller, opt...)
if err != nil {
return errors.Wrap(err, op)
}
if err := eventer.writeObservation(ctx, e); err != nil {
return errors.Wrap(err, op)
}
return nil
}
// WriteError will write an error event. It will first check the
// ctx for an eventer, then try event.SysEventer() and if no eventer can be
// found an hclog.Logger will be created and used.
//
// The options WithId and WithRequestInfo are supported and all other options
// are ignored.
func WriteError(ctx context.Context, caller Op, e error, opt ...Option) {
const op = "event.WriteError"
// EventerFromContext will handle a nil ctx appropriately. If e or caller is
// missing, newError(...) will handle them appropriately.
eventer, ok := EventerFromContext(ctx)
if !ok {
eventer = SysEventer()
if eventer == nil {
logger := hclog.New(nil)
logger.Error(fmt.Sprintf("%s: no eventer available to write error: %s", op, e))
return
}
}
opts := getOpts(opt...)
if opts.withRequestInfo == nil {
var err error
if opt, err = addCtxOptions(ctx, opt...); err != nil {
eventer.logger.Error(errors.Wrap(err, op).Error())
eventer.logger.Error(fmt.Sprintf("%s: unable to process context options to write error: %s", op, e))
return
}
}
ev, err := newError(caller, e, opt...)
if err != nil {
eventer.logger.Error(errors.Wrap(err, op).Error())
eventer.logger.Error(fmt.Sprintf("%s: unable to create new error to write error: %s", op, e))
return
}
if err := eventer.writeError(ctx, ev); err != nil {
eventer.logger.Error(errors.Wrap(err, op).Error())
eventer.logger.Error(fmt.Sprintf("%s: unable to write error: %s", op, e))
return
}
}
// WriteAudit will write an audit event. It will first check the ctx for an
// eventer, then try event.SysEventer() and if no eventer can be found an error
// is returned.
//
// At least one and any combination of the supported options may be used:
// WithRequest, WithResponse, WithAuth, WithId, WithFlush and WithRequestInfo.
// All other options are ignored.
func WriteAudit(ctx context.Context, caller Op, opt ...Option) error {
const op = "event.WriteAudit"
if ctx == nil {
return errors.New(errors.InvalidParameter, op, "missing context")
}
if caller == "" {
return errors.New(errors.InvalidParameter, op, "missing operation")
}
eventer, ok := EventerFromContext(ctx)
if !ok {
eventer = SysEventer()
if eventer == nil {
return errors.New(errors.InvalidParameter, op, "missing both context and system eventer")
}
}
opts := getOpts(opt...)
if opts.withRequestInfo == nil {
var err error
if opt, err = addCtxOptions(ctx, opt...); err != nil {
return errors.Wrap(err, op)
}
}
e, err := newAudit(caller, opt...)
if err != nil {
return errors.Wrap(err, op)
}
if err := eventer.writeAudit(ctx, e); err != nil {
return errors.Wrap(err, op)
}
return nil
}
func addCtxOptions(ctx context.Context, opt ...Option) ([]Option, error) {
const op = "event.addCtxOptions"
opts := getOpts(opt...)
retOpts := make([]Option, 0, len(opt))
retOpts = append(retOpts, opt...)
if opts.withRequestInfo == nil {
reqInfo, ok := RequestInfoFromContext(ctx)
if !ok {
// there's no RequestInfo, so there's no id associated with the
// observation and we'll generate one and flush the observation
// since there will never be another with the same id
id, err := newId(string(ObservationType))
if err != nil {
return nil, errors.Wrap(err, op)
}
retOpts = append(retOpts, WithId(id))
if !opts.withFlush {
retOpts = append(retOpts, WithFlush())
}
return retOpts, nil
}
retOpts = append(retOpts, WithRequestInfo(reqInfo))
if reqInfo.Id != "" {
retOpts = append(retOpts, WithId(reqInfo.Id))
}
switch reqInfo.Id {
case "":
// there's no RequestInfo.Id associated with the observation, so we'll
// generate one and flush the observation since there will never be
// another with the same id
id, err := newId(string(ObservationType))
if err != nil {
return nil, errors.Wrap(err, op)
}
retOpts = append(retOpts, WithId(id))
if !opts.withFlush {
retOpts = append(retOpts, WithFlush())
}
return retOpts, nil
default:
retOpts = append(retOpts, WithId(reqInfo.Id))
}
}
return retOpts, nil
}

@ -0,0 +1,903 @@
package event_test
import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"testing"
"time"
"github.com/golang/protobuf/ptypes/wrappers"
"github.com/hashicorp/boundary/internal/errors"
pb "github.com/hashicorp/boundary/internal/gen/controller/api/resources/hosts"
"github.com/hashicorp/boundary/internal/gen/controller/api/resources/scopes"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/boundary/internal/host/static"
"github.com/hashicorp/boundary/internal/observability/event"
"github.com/hashicorp/boundary/internal/types/scope"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/structpb"
)
const apiRequest = "APIRequest"
const testAuditVersion = "v0.1"
const testErrorVersion = "v0.1"
const 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
SerializedHMAC string `json:"serialized_hmac"` // boundary field
Flush bool `json:"-"`
}
func Test_NewRequestInfoContext(t *testing.T) {
testInfo := event.TestRequestInfo(t)
testInfoMissingId := event.TestRequestInfo(t)
testInfoMissingId.Id = ""
tests := []struct {
name string
ctx context.Context
requestInfo *event.RequestInfo
wantErrMatch *errors.Template
wantErrContains string
}{
{
name: "missing-ctx",
requestInfo: testInfo,
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing context",
},
{
name: "missing-request-info",
ctx: context.Background(),
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing request info",
},
{
name: "missing-request-info-id",
ctx: context.Background(),
requestInfo: testInfoMissingId,
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing request info 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.wantErrMatch != nil {
require.Errorf(err, "should have gotten an error")
assert.Nilf(ctx, "context should be nil")
assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted %q and got %q", tt.wantErrMatch, err)
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")
testEventer, err := event.NewEventer(hclog.Default(), testSetup.EventerConfig)
require.NoError(t, err)
tests := []struct {
name string
ctx context.Context
eventer *event.Eventer
wantErrMatch *errors.Template
wantErrContains string
}{
{
name: "missing-ctx",
eventer: testEventer,
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing context",
},
{
name: "missing-eventer",
ctx: context.Background(),
wantErrMatch: errors.T(errors.InvalidParameter),
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.wantErrMatch != nil {
require.Errorf(err, "should have gotten an error")
assert.Nilf(ctx, "context should be nil")
assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted %q and got %q", tt.wantErrMatch, err)
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")
testEventer, err := event.NewEventer(hclog.Default(), 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
logger := hclog.New(&hclog.LoggerOptions{
Name: "test",
})
c := event.TestEventerConfig(t, "WriteObservation")
e, err := event.NewEventer(logger, c.EventerConfig)
require.NoError(t, err)
info := &event.RequestInfo{Id: "867-5309"}
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"}
testCtxNoInfoId, err = event.NewRequestInfoContext(testCtxNoInfoId, noId)
require.NoError(t, err)
noId.Id = ""
type observationPayload struct {
header map[string]interface{}
details map[string]interface{}
}
testPayloads := []observationPayload{
{
header: map[string]interface{}{
"name": "bar",
},
},
{
header: map[string]interface{}{
"list": []string{"1", "2"},
},
},
{
details: map[string]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()
wantErrMatch *errors.Template
wantErrContains string
}{
{
name: "no-info-id",
noFlush: true,
ctx: testCtxNoInfoId,
observationPayload: []observationPayload{
{
header: map[string]interface{}{
"name": "bar",
},
},
},
header: map[string]interface{}{
"name": "bar",
},
observationSinkFileName: c.AllEvents.Name(),
setup: func() error {
return event.InitSysEventer(hclog.Default(), c.EventerConfig)
},
cleanup: func() { event.TestResetSystEventer(t) },
},
{
name: "missing-ctx",
observationPayload: testPayloads,
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing context",
},
{
name: "missing-op",
ctx: testCtx,
noOperation: true,
observationPayload: testPayloads,
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing operation",
},
{
name: "no-header-or-details-in-payload",
noFlush: true,
ctx: testCtx,
observationPayload: []observationPayload{
{},
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "you must specify either header or details options",
},
{
name: "no-ctx-eventer-and-syseventer-not-initialized",
ctx: context.Background(),
observationPayload: testPayloads,
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing both context and system eventer",
},
{
name: "use-syseventer",
noFlush: true,
ctx: context.Background(),
observationPayload: []observationPayload{
{
header: map[string]interface{}{
"name": "bar",
},
},
},
header: map[string]interface{}{
"name": "bar",
},
observationSinkFileName: c.AllEvents.Name(),
setup: func() error {
return event.InitSysEventer(hclog.Default(), 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.wantErrMatch != nil {
require.Errorf(err, "wanted error %q", tt.wantErrMatch)
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 := &eventJson{}
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)
logger := hclog.New(&hclog.LoggerOptions{
Name: "test",
})
c := event.TestEventerConfig(t, "WriteObservation")
c.EventerConfig.ObservationsEnabled = false
e, err := event.NewEventer(logger, 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 testObservationJsonFromCtx(t *testing.T, ctx context.Context, caller event.Op, got *eventJson, hdr, details map[string]interface{}) []byte {
t.Helper()
require := require.New(t)
reqInfo, _ := event.RequestInfoFromContext(ctx)
// require.Truef(ok, "missing reqInfo in ctx")
j := eventJson{
CreatedAt: got.CreatedAt,
EventType: string(event.ObservationType),
Payload: map[string]interface{}{
event.IdField: got.Payload[event.IdField].(string),
event.HeaderField: map[string]interface{}{
event.RequestInfoField: reqInfo,
event.VersionField: testObservationVersion,
},
},
}
if hdr != nil {
h := j.Payload[event.HeaderField].(map[string]interface{})
for k, v := range hdr {
h[k] = v
}
}
if details != nil {
details[event.OpField] = string(caller)
d := got.Payload[event.DetailsField].([]interface{})[0].(map[string]interface{})
j.Payload[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
}
type eventJson struct {
CreatedAt string `json:"created_at"`
EventType string `json:"event_type"`
Payload map[string]interface{} `json:"payload"`
}
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()
logger := hclog.New(&hclog.LoggerOptions{
Name: "test",
})
c := event.TestEventerConfig(t, "WriteAudit")
e, err := event.NewEventer(logger, c.EventerConfig, event.WithNow(now))
require.NoError(t, err)
info := &event.RequestInfo{Id: "867-5309"}
ctx, err := event.NewEventerContext(context.Background(), e)
require.NoError(t, err)
ctx, err = event.NewRequestInfoContext(ctx, info)
require.NoError(t, err)
testAuth := &event.Auth{}
testReq := &event.Request{
Operation: "POST",
Endpoint: "/v1/hosts",
Details: &pbs.CreateHostRequest{Item: &pb.Host{
HostCatalogId: "hc_1234567890",
Name: &wrappers.StringValue{Value: "name"},
Description: &wrappers.StringValue{Value: "desc"},
Type: "static",
Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{
"address": structpb.NewStringValue("123.456.789"),
}},
}},
}
testAuthorizedActions := []string{"no-op", "read", "update", "delete"}
testResp := &event.Response{
StatusCode: 200,
Details: &pbs.CreateHostResponse{
Uri: fmt.Sprintf("hosts/%s_", static.HostPrefix),
Item: &pb.Host{
HostCatalogId: "hc_1234567890",
Scope: &scopes.ScopeInfo{Id: "proj_1234567890", Type: scope.Project.String(), ParentScopeId: "org_1234567890"},
Name: &wrappers.StringValue{Value: "name"},
Description: &wrappers.StringValue{Value: "desc"},
Type: "static",
Attributes: &structpb.Struct{Fields: map[string]*structpb.Value{
"address": structpb.NewStringValue("123.456.789"),
}},
AuthorizedActions: testAuthorizedActions,
},
},
}
tests := []struct {
name string
auditOpts [][]event.Option
wantAudit *testAudit
ctx context.Context
auditSinkFileName string
setup func() error
cleanup func()
noOperation bool
noFlush bool
wantErrMatch *errors.Template
wantErrContains string
}{
{
name: "missing-ctx",
auditOpts: [][]event.Option{
{
event.WithAuth(testAuth),
event.WithRequest(testReq),
},
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing context",
},
{
name: "missing-op",
ctx: ctx,
auditOpts: [][]event.Option{
{
event.WithAuth(testAuth),
event.WithRequest(testReq),
},
},
noOperation: true,
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing operation",
},
{
name: "no-ctx-eventer-and-syseventer-not-initialized",
ctx: context.Background(),
auditOpts: [][]event.Option{
{
event.WithAuth(testAuth),
event.WithRequest(testReq),
},
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing both context and system eventer",
},
{
name: "use-syseventer",
noFlush: true,
ctx: context.Background(),
auditOpts: [][]event.Option{
{
event.WithAuth(testAuth),
event.WithRequest(testReq),
},
},
wantAudit: &testAudit{
Auth: testAuth,
Request: testReq,
},
setup: func() error {
return event.InitSysEventer(hclog.Default(), 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: "867-5309",
Auth: testAuth,
Request: testReq,
Response: testResp,
},
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.wantErrMatch != nil {
require.Errorf(err, "wanted error %q", tt.wantErrMatch)
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 := &eventJson{}
err = json.Unmarshal(b, gotAudit)
require.NoErrorf(err, "json: %s", string(b))
actualJson, err := json.Marshal(gotAudit)
require.NoError(err)
wantEvent := eventJson{
CreatedAt: gotAudit.CreatedAt,
EventType: string(gotAudit.EventType),
Payload: map[string]interface{}{
"auth": tt.wantAudit.Auth,
"id": gotAudit.Payload["id"],
"timestamp": now,
"request": tt.wantAudit.Request,
"serialized_hmac": "",
"type": apiRequest,
"version": testAuditVersion,
},
}
if tt.wantAudit.Id != "" {
wantEvent.Payload["id"] = tt.wantAudit.Id
wantEvent.Payload["request_info"] = event.RequestInfo{
Id: tt.wantAudit.Id,
}
}
if tt.wantAudit.Response != nil {
wantEvent.Payload["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)
logger := hclog.New(&hclog.LoggerOptions{
Name: "test",
})
c := event.TestEventerConfig(t, "WriteAudit")
c.EventerConfig.AuditEnabled = false
e, err := event.NewEventer(logger, 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()
logger := hclog.New(&hclog.LoggerOptions{
Name: "test",
})
c := event.TestEventerConfig(t, "WriteAudit")
e, err := event.NewEventer(logger, c.EventerConfig, event.WithNow(now))
require.NoError(t, err)
info := &event.RequestInfo{Id: "867-5309"}
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"}
testCtxNoInfoId, err = event.NewRequestInfoContext(testCtxNoInfoId, noId)
require.NoError(t, err)
noId.Id = ""
testError := errors.New(errors.InvalidParameter, "test", "you lookin at me?")
tests := []struct {
name string
ctx context.Context
e error
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(hclog.Default(), 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(hclog.Default(), c.EventerConfig)
},
cleanup: func() { event.TestResetSystEventer(t) },
errSinkFileName: c.ErrorEvents.Name(),
},
{
name: "simple",
ctx: testCtx,
e: testError,
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)
if tt.errSinkFileName != "" {
defer func() { _ = os.WriteFile(tt.errSinkFileName, nil, 0666) }()
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 := &eventJson{}
err = json.Unmarshal(b, gotError)
require.NoErrorf(err, "json: %s", string(b))
actualJson, err := json.Marshal(gotError)
require.NoError(err)
wantError := eventJson{
CreatedAt: gotError.CreatedAt,
EventType: string(gotError.EventType),
Payload: map[string]interface{}{
"error": map[string]interface{}{
"Code": tt.e.(*errors.Err).Code,
"Msg": tt.e.(*errors.Err).Msg,
"Op": tt.e.(*errors.Err).Op,
"Wrapped": tt.e.(*errors.Err).Wrapped,
},
"id": gotError.Payload["id"],
"op": op,
"version": testErrorVersion,
},
}
if tt.info != nil && tt.info.Id != "" {
wantError.Payload["id"] = tt.info.Id
}
if tt.info != nil {
wantError.Payload["request_info"] = tt.info
}
wantJson, err := json.Marshal(wantError)
require.NoError(err)
assert.JSONEq(string(wantJson), string(actualJson))
}
})
}
}

@ -0,0 +1,51 @@
package event
import "google.golang.org/protobuf/proto"
type (
Id string
Op string
)
// RequestInfo defines the fields captured about a Boundary request.
type RequestInfo struct {
Id string `json:"id,omitempty"`
Method string `json:"method,omitempty"`
Path string `json:"path,omitempty"`
PublicId string `json:"public_id,omitempty"`
}
// UserInfo defines the fields captured about a user for a Boundary request.
type UserInfo struct {
UserId string `json:"id,omitempty"`
AuthAccountId string `json:"auth_account_id,omitempty"`
}
type GrantsInfo struct {
Grants []GrantsPair `json:"grants_pair,omitempty"`
}
type GrantsPair struct {
Grant string `json:"grant,omitempty"`
ScopeId string `json:"scope_id,omitempty"`
}
type Auth struct {
// AccessorId is a std audit field == auth_token public_id
AccessorId string `json:"accessor_id"`
UserInfo *UserInfo `json:"user_info,omitempty"` // boundary field
GrantsInfo *GrantsInfo `json:"grants_info,omitempty"`
UserEmail string `json:"email,omitempty"`
UserName string `json:"name,omitempty"`
}
type Request struct {
Operation string `json:"operation"` // std audit field
Endpoint string `json:"endpoint"` // std audit field
Details proto.Message `json:"details"` // boundary field
}
type Response struct {
StatusCode int `json:"status_code,omitempty"` // std audit
Details proto.Message `json:"details,omitempty"` // boundary field
}

@ -0,0 +1,148 @@
package event
import (
"fmt"
"time"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/eventlogger"
)
// auditVersion defines the version of audit events
const auditVersion = "v0.1"
// auditEventType defines the type of audit event
type auditEventType string
const (
apiRequest auditEventType = "APIRequest" // ApiRequest defines an API request audit event type
)
// audit defines the data of audit events
type audit 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 *RequestInfo `json:"request_info,omitempty"` // boundary field
Auth *Auth `json:"auth,omitempty"` // std audit field
Request *Request `json:"request,omitempty"` // std audit field
Response *Response `json:"response,omitempty"` // std audit field
SerializedHMAC string `json:"serialized_hmac"` // boundary field
Flush bool `json:"-"`
}
func newAudit(fromOperation Op, opt ...Option) (*audit, error) {
const op = "event.newAudit"
if fromOperation == "" {
return nil, errors.New(errors.InvalidParameter, op, "missing from operation")
}
opts := getOpts(opt...)
if opts.withId == "" {
var err error
opts.withId, err = newId(string(AuditType))
if err != nil {
return nil, errors.Wrap(err, op)
}
}
var dtm time.Time
switch opts.withNow.IsZero() {
case false:
dtm = opts.withNow
default:
dtm = time.Now()
}
a := &audit{
Id: opts.withId,
Version: auditVersion,
Type: string(apiRequest),
Timestamp: dtm,
RequestInfo: opts.withRequestInfo,
Auth: opts.withAuth,
Request: opts.withRequest,
Response: opts.withResponse,
Flush: opts.withFlush,
}
if err := a.validate(); err != nil {
return nil, errors.Wrap(err, op)
}
return a, nil
}
// EventType is required for all event types by the eventlogger broker
func (a *audit) EventType() string { return string(AuditType) }
func (a *audit) validate() error {
const op = "event.(audit).validate"
if a.Id == "" {
return errors.New(errors.InvalidParameter, op, "missing id")
}
return nil
}
// GetID is part of the eventlogger.Gateable interface and returns the audit
// event's id.
func (a *audit) GetID() string {
return a.Id
}
// FlushEvent is part of the eventlogger.Gateable interface and returns the
// value of the audit event's flush field
func (a *audit) FlushEvent() bool {
return a.Flush
}
// ComposedFrom is part of the eventlogger.Gatable interface. It's important to
// remember that the receiver will always be nil when this is called by the eventlogger.GatedFilter
func (a *audit) ComposeFrom(events []*eventlogger.Event) (eventlogger.EventType, interface{}, error) {
const op = "event.(audit).ComposedFrom"
if len(events) == 0 {
return "", nil, errors.New(errors.InvalidParameter, op, "missing events")
}
var validId string
payload := audit{}
for i, v := range events {
gated, ok := v.Payload.(*audit)
if !ok {
return "", nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("event %d is not an audit payload", i))
}
if gated.Id == "" {
// can't really happen since it has to have an id to be gated, but
// I'll add this check in the name of completeness
return "", nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("event %d: id is required", i))
}
if validId == "" {
validId = gated.Id
}
if gated.Id != validId {
return "", nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("event %d has an invalid id: %s != %s", i, gated.Id, validId))
}
if gated.Version != auditVersion {
return "", nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("event %d has an invalid version: %s != %s", i, gated.Version, auditVersion))
}
if gated.Type != string(apiRequest) {
return "", nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("event %d has an invalid type: %s != %s", i, gated.Type, string(AuditType)))
}
if gated.RequestInfo != nil {
payload.RequestInfo = gated.RequestInfo
}
if gated.Auth != nil {
payload.Auth = gated.Auth
}
if gated.Request != nil {
payload.Request = gated.Request
}
if gated.Response != nil {
payload.Response = gated.Response
}
if !gated.Timestamp.IsZero() {
payload.Timestamp = gated.Timestamp
}
}
payload.Id = validId
payload.Version = auditVersion
payload.Type = string(apiRequest)
return eventlogger.EventType(a.EventType()), payload, nil
}

@ -0,0 +1,278 @@
package event
import (
"testing"
"time"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/eventlogger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_newAudit(t *testing.T) {
t.Parallel()
testNow := time.Now()
tests := []struct {
name string
fromOp Op
opts []Option
want *audit
wantErrMatch *errors.Template
}{
{
name: "missing-op",
wantErrMatch: errors.T(errors.InvalidParameter),
},
{
name: "valid-no-opts",
fromOp: "valid-no-opts",
want: &audit{
Version: auditVersion,
Type: string(apiRequest),
},
},
{
name: "all-opts",
fromOp: "all-opts",
opts: []Option{
WithId("all-opts"),
WithNow(testNow),
WithRequestInfo(TestRequestInfo(t)),
WithAuth(testAuth(t)),
WithRequest(testRequest(t)),
WithResponse(testResponse(t)),
WithFlush(),
},
want: &audit{
Id: "all-opts",
Version: auditVersion,
Type: string(apiRequest),
Timestamp: testNow,
RequestInfo: TestRequestInfo(t),
Auth: testAuth(t),
Request: testRequest(t),
Response: testResponse(t),
Flush: true,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := newAudit(tt.fromOp, tt.opts...)
if tt.wantErrMatch != nil {
require.Error(err)
assert.Nil(got)
assert.True(errors.Match(tt.wantErrMatch, err))
return
}
require.NoError(err)
require.NotNil(got)
opts := getOpts(tt.opts...)
if opts.withId == "" {
tt.want.Id = got.Id
}
if opts.withNow.IsZero() {
tt.want.Timestamp = got.Timestamp
}
assert.Equal(tt.want, got)
})
}
}
func TestAudit_validate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
id string
wantErrMatch *errors.Template
wantErrContains string
}{
{
name: "missing-id",
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing id",
},
{
name: "valid",
id: "valid",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
a := audit{Id: tt.id}
err := a.validate()
if tt.wantErrMatch != nil {
require.Error(err)
assert.True(errors.Match(tt.wantErrMatch, err))
if tt.wantErrContains != "" {
assert.Contains(err.Error(), tt.wantErrContains)
}
return
}
assert.NoError(err)
})
}
}
func TestAudit_EventType(t *testing.T) {
t.Parallel()
a := &audit{}
assert.Equal(t, string(AuditType), a.EventType())
}
func TestAudit_GetID(t *testing.T) {
t.Parallel()
a := &audit{Id: "test"}
assert.Equal(t, "test", a.GetID())
}
func TestAudit_FlushEvent(t *testing.T) {
t.Parallel()
a := &audit{Flush: true}
assert.True(t, a.FlushEvent())
a.Flush = false
assert.False(t, a.FlushEvent())
}
func TestAudit_ComposeFrom(t *testing.T) {
t.Parallel()
testNow := time.Now()
tests := []struct {
name string
events []*eventlogger.Event
want audit
wantErrMatch *errors.Template
wantErrContains string
}{
{
name: "missing-events",
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing events",
},
{
name: "not-an-audit",
events: []*eventlogger.Event{{
Payload: struct{}{},
}},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "not an audit payload",
},
{
name: "invalid-type",
events: []*eventlogger.Event{
{
Payload: &audit{
Id: "test-id",
Version: auditVersion,
Type: "invalid-type",
},
},
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "invalid type",
},
{
name: "invalid-version",
events: []*eventlogger.Event{
{
Payload: &audit{
Id: "test-id",
Version: "invalid-version",
Type: string(apiRequest),
},
},
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "invalid version",
},
{
name: "invalid-id",
events: []*eventlogger.Event{
{
Payload: &audit{
Id: "invalid-id",
Version: auditVersion,
Type: string(apiRequest),
},
},
{
Payload: &audit{
Id: "bad-id",
Version: auditVersion,
Type: string(apiRequest),
},
},
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "invalid id",
},
{
name: "valid",
events: []*eventlogger.Event{
{
Payload: &audit{
Id: "valid",
Version: auditVersion,
Type: string(apiRequest),
Timestamp: testNow,
Auth: testAuth(t),
RequestInfo: TestRequestInfo(t),
},
},
{
Payload: &audit{
Id: "valid",
Version: auditVersion,
Type: string(apiRequest),
Timestamp: testNow,
Request: testRequest(t),
},
},
{
Payload: &audit{
Id: "valid",
Version: auditVersion,
Type: string(apiRequest),
Timestamp: testNow,
Response: testResponse(t),
},
},
},
want: audit{
Id: "valid",
Version: auditVersion,
Type: string(apiRequest),
Timestamp: testNow,
Auth: testAuth(t),
Request: testRequest(t),
Response: testResponse(t),
RequestInfo: TestRequestInfo(t),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
a := &audit{}
gotType, gotAudit, err := a.ComposeFrom(tt.events)
if tt.wantErrMatch != nil {
require.Error(err)
assert.Nil(gotAudit)
assert.True(errors.Match(tt.wantErrMatch, err))
if tt.wantErrContains != "" {
assert.Contains(err.Error(), tt.wantErrContains)
}
return
}
require.NoError(err)
require.NotNil(gotAudit)
assert.Equal(eventlogger.EventType(a.EventType()), gotType)
tt.want.Timestamp = gotAudit.(audit).Timestamp
assert.Equal(tt.want, gotAudit)
})
}
}

@ -0,0 +1,25 @@
package event
import (
"fmt"
"github.com/hashicorp/boundary/internal/errors"
)
const (
DefaultDeliveryGuarantee DeliveryGuarantee = "" // DefaultDeliveryGuarantee will be BestEffort
Enforced DeliveryGuarantee = "enforced" // Enforced means that a delivery guarantee is enforced
BestEffort DeliveryGuarantee = "best-effort" // BestEffort means that a best effort will be made to deliver an event
)
type DeliveryGuarantee string // DeliveryGuarantee defines the guarantees around delivery of an event type within config
func (g DeliveryGuarantee) validate() error {
const op = "event.(DeliveryGuarantee).validate"
switch g {
case DefaultDeliveryGuarantee, BestEffort, Enforced:
return nil
default:
return errors.New(errors.InvalidParameter, op, fmt.Sprintf("%s is not a valid delivery guarantee", g))
}
}

@ -0,0 +1,53 @@
package event
import (
"testing"
"github.com/hashicorp/boundary/internal/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDeliveryGuarantee_validate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
g DeliveryGuarantee
wantErrMatch *errors.Template
wantErrContains string
}{
{
name: "invalid",
g: "invalid",
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "not a valid delivery guarantee",
},
{
name: "BestEffort",
g: BestEffort,
},
{
name: "Default",
g: DefaultDeliveryGuarantee,
},
{
name: "Enforced",
g: Enforced,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
err := tt.g.validate()
if tt.wantErrMatch != nil {
require.Error(err)
assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted %q and got %q", tt.wantErrMatch, err)
if tt.wantErrContains != "" {
assert.Contains(err.Error(), tt.wantErrContains)
}
return
}
require.NoError(err)
})
}
}

@ -0,0 +1,60 @@
package event
import "github.com/hashicorp/boundary/internal/errors"
// errorVersion defines the version of error events
const errorVersion = "v0.1"
type err struct {
Error error `json:"error"`
Id Id `json:"id,omitempty"`
Version string `json:"version"`
Op Op `json:"op,omitempty"`
RequestInfo *RequestInfo `json:"request_info,omitempty"`
}
func newError(fromOperation Op, e error, opt ...Option) (*err, error) {
const op = "event.newError"
if fromOperation == "" {
return nil, errors.New(errors.InvalidParameter, op, "missing operation")
}
if e == nil {
return nil, errors.New(errors.InvalidParameter, op, "missing error")
}
opts := getOpts(opt...)
if opts.withId == "" {
var err error
opts.withId, err = newId(string(ErrorType))
if err != nil {
return nil, errors.Wrap(err, op)
}
}
newErr := &err{
Id: Id(opts.withId),
Op: fromOperation,
Version: errorVersion,
RequestInfo: opts.withRequestInfo,
Error: e,
}
if err := newErr.validate(); err != nil {
return nil, errors.Wrap(err, op)
}
return newErr, nil
}
// EventType is required for all event types by the eventlogger broker
func (e *err) EventType() string { return string(ErrorType) }
func (e *err) validate() error {
const op = "event.(err).validate"
if e.Id == "" {
return errors.New(errors.InvalidParameter, op, "missing id")
}
if e.Op == "" {
return errors.New(errors.InvalidParameter, op, "missing operation")
}
if e.Error == nil {
return errors.New(errors.InvalidParameter, op, "missing error")
}
return nil
}

@ -0,0 +1,147 @@
package event
import (
"testing"
"github.com/hashicorp/boundary/internal/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_newError(t *testing.T) {
t.Parallel()
tests := []struct {
name string
fromOp Op
e error
opts []Option
want *err
wantErrMatch *errors.Template
wantErrContains string
}{
{
name: "missing-op",
e: errors.New(errors.InvalidParameter, "missing-operation", "missing operation"),
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing operation",
},
{
name: "missing-error",
fromOp: Op("missing-error"),
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing error",
},
{
name: "valid-no-opts",
fromOp: Op("valid-no-opts"),
e: errors.New(errors.InvalidParameter, "valid-no-opts", "valid no opts"),
want: &err{
Error: errors.New(errors.InvalidParameter, "valid-no-opts", "valid no opts"),
Version: errorVersion,
Op: Op("valid-no-opts"),
},
},
{
name: "valid-all-opts",
fromOp: Op("valid-all-opts"),
e: errors.New(errors.InvalidParameter, "valid-all-opts", "valid all opts"),
opts: []Option{
WithId("valid-all-opts"),
WithRequestInfo(TestRequestInfo(t)),
},
want: &err{
Error: errors.New(errors.InvalidParameter, "valid-all-opts", "valid all opts"),
Version: errorVersion,
Op: Op("valid-all-opts"),
Id: "valid-all-opts",
RequestInfo: TestRequestInfo(t),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := newError(tt.fromOp, tt.e, tt.opts...)
if tt.wantErrMatch != nil {
require.Error(err)
assert.Nil(got)
assert.True(errors.Match(tt.wantErrMatch, err))
if tt.wantErrContains != "" {
assert.Contains(err.Error(), tt.wantErrContains)
}
return
}
require.NoError(err)
require.NotNil(got)
opts := getOpts(tt.opts...)
if opts.withId == "" {
tt.want.Id = got.Id
}
if opts.withRequestInfo != nil {
tt.want.RequestInfo = got.RequestInfo
}
assert.Equal(tt.want, got)
})
}
}
func Test_errvalidate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
id string
op Op
want error
wantErrMatch *errors.Template
wantErrContains string
}{
{
name: "missing-id",
op: Op("missing-id"),
want: errors.New(errors.InvalidParameter, "missing-id", "missing id"),
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing id",
},
{
name: "missing-operation",
id: "missing-operation",
want: errors.New(errors.InvalidParameter, "missing-operation", "missing operation"),
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing operation",
},
{
name: "valid",
op: Op("valid"),
id: "valid",
want: errors.New(errors.InvalidParameter, "valid", "valid error"),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
e := err{
Op: tt.op,
Id: Id(tt.id),
Error: tt.want,
}
err := e.validate()
if tt.wantErrMatch != nil {
require.Error(err)
assert.True(errors.Match(tt.wantErrMatch, err))
if tt.wantErrContains != "" {
assert.Contains(err.Error(), tt.wantErrContains)
}
return
}
assert.NoError(err)
assert.Equal(tt.want, e.Error)
})
}
}
func Test_errEventType(t *testing.T) {
t.Parallel()
e := &err{}
assert.Equal(t, string(ErrorType), e.EventType())
}

@ -0,0 +1,68 @@
package event
import (
"fmt"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/eventlogger"
"github.com/hashicorp/vault/sdk/helper/strutil"
)
// observationVersion defines the version of observation events
const observationVersion = "v0.1"
type observation struct {
*eventlogger.SimpleGatedPayload
Version string `json:"version"`
Op Op `json:"op,omitempty"`
RequestInfo *RequestInfo `json:"request_info,omitempty"`
}
func newObservation(fromOperation Op, opt ...Option) (*observation, error) {
const op = "event.newObservation"
if fromOperation == "" {
return nil, errors.New(errors.InvalidParameter, op, "missing operation")
}
opts := getOpts(opt...)
if opts.withId == "" {
var err error
opts.withId, err = newId(string(ObservationType))
if err != nil {
return nil, errors.Wrap(err, op)
}
}
for k := range opts.withHeader {
if strutil.StrListContains([]string{OpField, VersionField, RequestInfoField}, k) {
return nil, errors.New(errors.InvalidParameter, op, fmt.Sprintf("%s is a reserved field name", k))
}
}
i := &observation{
SimpleGatedPayload: &eventlogger.SimpleGatedPayload{
ID: opts.withId,
Header: opts.withHeader,
Detail: opts.withDetails,
Flush: opts.withFlush,
},
Op: fromOperation,
RequestInfo: opts.withRequestInfo,
Version: observationVersion,
}
if err := i.validate(); err != nil {
return nil, errors.Wrap(err, op)
}
return i, nil
}
// EventType is required for all event types by the eventlogger broker
func (i *observation) EventType() string { return string(ObservationType) }
func (i *observation) validate() error {
const op = "event.(Observation).validate"
if i.ID == "" {
return errors.New(errors.InvalidParameter, op, "missing id")
}
if i.Op == "" {
return errors.New(errors.InvalidParameter, op, "missing operation")
}
return nil
}

@ -0,0 +1,150 @@
package event
import (
"testing"
"time"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/eventlogger"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_newObservation(t *testing.T) {
t.Parallel()
now := time.Now()
testHeader := map[string]interface{}{
"public-id": "public-id",
"now": now,
}
testDetails := map[string]interface{}{
"file_name": "tmpfile-name",
}
tests := []struct {
name string
fromOp Op
opts []Option
want *observation
wantErrMatch *errors.Template
wantErrContains string
}{
{
name: "missing-op",
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing operation",
},
{
name: "valid-no-opts",
fromOp: Op("valid-no-opts"),
want: &observation{
SimpleGatedPayload: &eventlogger.SimpleGatedPayload{},
Version: errorVersion,
Op: Op("valid-no-opts"),
},
},
{
name: "valid-all-opts",
fromOp: Op("valid-all-opts"),
opts: []Option{
WithId("valid-all-opts"),
WithRequestInfo(TestRequestInfo(t)),
WithHeader(testHeader),
WithDetails(testDetails),
WithFlush(),
},
want: &observation{
SimpleGatedPayload: &eventlogger.SimpleGatedPayload{
ID: "valid-all-opts",
Header: testHeader,
Detail: testDetails,
Flush: true,
},
Version: errorVersion,
Op: Op("valid-all-opts"),
RequestInfo: TestRequestInfo(t),
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := newObservation(tt.fromOp, tt.opts...)
if tt.wantErrMatch != nil {
require.Error(err)
assert.Nil(got)
assert.True(errors.Match(tt.wantErrMatch, err))
if tt.wantErrContains != "" {
assert.Contains(err.Error(), tt.wantErrContains)
}
return
}
require.NoError(err)
require.NotNil(got)
opts := getOpts(tt.opts...)
if opts.withId == "" {
tt.want.ID = got.ID
}
assert.Equal(tt.want, got)
})
}
}
func Test_observationvalidate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
id string
op Op
wantErrMatch *errors.Template
wantErrContains string
}{
{
name: "missing-id",
op: Op("missing-id"),
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing id",
},
{
name: "missing-operation",
id: "missing-operation",
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing operation",
},
{
name: "valid",
op: Op("valid"),
id: "valid",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
e := observation{
Op: tt.op,
SimpleGatedPayload: &eventlogger.SimpleGatedPayload{
ID: tt.id,
},
}
err := e.validate()
if tt.wantErrMatch != nil {
require.Error(err)
assert.True(errors.Match(tt.wantErrMatch, err))
if tt.wantErrContains != "" {
assert.Contains(err.Error(), tt.wantErrContains)
}
return
}
assert.NoError(err)
})
}
}
func Test_observationEventType(t *testing.T) {
t.Parallel()
e := &observation{}
assert.Equal(t, string(ObservationType), e.EventType())
}

@ -0,0 +1,27 @@
package event
import (
"fmt"
"github.com/hashicorp/boundary/internal/errors"
)
// Type represents the event's type
type Type string
const (
EveryType Type = "*" // EveryType represents every (all) types of events
ObservationType Type = "observation" // ObservationType represents observation events
AuditType Type = "audit" // AuditType represents audit events
ErrorType Type = "error" // ErrorType represents error events
)
func (et Type) validate() error {
const op = "event.(Type).validate"
switch et {
case EveryType, ObservationType, AuditType, ErrorType:
return nil
default:
return errors.New(errors.InvalidParameter, op, fmt.Sprintf("'%s' is not a valid event type", et))
}
}

@ -0,0 +1,398 @@
package event
import (
"context"
"fmt"
"os"
"sync"
"time"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/eventlogger"
"github.com/hashicorp/go-hclog"
)
const (
OpField = "op" // OpField in an event.
RequestInfoField = "request_info" // RequestInfoField in an event.
VersionField = "version" // VersionField in an event
DetailsField = "details" // Details field in an event.
HeaderField = "header" // HeaderField in an event.
IdField = "id" // IdField in an event.
CreatedAtField = "created_at" // CreatedAtField in an event.
TypeField = "type" // TypeField in an event.
auditPipeline = "audit-pipeline" // auditPipeline is a pipeline for audit events
observationPipeline = "observation-pipeline" // observationPipeline is a pipeline for observation events
errPipeline = "err-pipeline" // errPipeline is a pipeline for error events
)
// flushable defines an interface that all eventlogger Nodes must implement if
// they are "flushable"
type flushable interface {
FlushAll(ctx context.Context) error
}
// broker defines an interface for an eventlogger Broker... which will allow us
// to substitute our testing broker when needed to write tests for things
// like event send retrying.
type broker interface {
Send(ctx context.Context, t eventlogger.EventType, payload interface{}) (eventlogger.Status, error)
Reopen(ctx context.Context) error
StopTimeAt(now time.Time)
RegisterNode(id eventlogger.NodeID, node eventlogger.Node) error
SetSuccessThreshold(t eventlogger.EventType, successThreshold int) error
RegisterPipeline(def eventlogger.Pipeline) error
}
// Eventer provides a method to send events to pipelines of sinks
type Eventer struct {
broker broker
flushableNodes []flushable
conf EventerConfig
logger hclog.Logger
}
var (
sysEventer *Eventer // sysEventer is the system-wide Eventer
sysEventerOnce sync.Once // sysEventerOnce ensures that the system-wide Eventer is only initialized once.
)
// InitSysEventer provides a mechanism to initialize a "system wide" eventer
// singleton for Boundary
func InitSysEventer(log hclog.Logger, c EventerConfig) error {
const op = "event.InitSysEventer"
if log == nil {
return errors.New(errors.InvalidParameter, op, "missing hclog")
}
// the order of operations is important here. we want to determine if
// there's an error before setting the singleton sysEventer which can only
// be done one time.
eventer, err := NewEventer(log, c)
if err != nil {
return errors.Wrap(err, op)
}
sysEventerOnce.Do(func() {
sysEventer = eventer
})
return nil
}
// SysEventer returns the "system wide" eventer for Boundary.
func SysEventer() *Eventer {
return sysEventer
}
// NewEventer creates a new Eventer using the config. Supports options: WithNow
func NewEventer(log hclog.Logger, c EventerConfig, opt ...Option) (*Eventer, error) {
const op = "event.NewEventer"
if log == nil {
return nil, errors.New(errors.InvalidParameter, op, "missing logger")
}
// if there are no sinks in config, then we'll default to just one stdout
// sink.
if len(c.Sinks) == 0 {
c.Sinks = append(c.Sinks, SinkConfig{
Name: "default",
EventTypes: []Type{EveryType},
Format: JSONSinkFormat,
SinkType: StdoutSink,
})
}
if err := c.validate(); err != nil {
return nil, errors.Wrap(err, op)
}
type pipeline struct {
eventType Type
fmtId eventlogger.NodeID
sinkId eventlogger.NodeID
}
var auditPipelines, observationPipelines, errPipelines []pipeline
opts := getOpts(opt...)
var b broker
switch {
case opts.withBroker != nil:
b = opts.withBroker
default:
b = eventlogger.NewBroker()
}
e := &Eventer{
logger: log,
conf: c,
broker: b,
}
if !opts.withNow.IsZero() {
e.broker.StopTimeAt(opts.withNow)
}
// Create JSONFormatter node
id, err := newId("json")
if err != nil {
return nil, errors.Wrap(err, op)
}
jsonfmtId := eventlogger.NodeID(id)
fmtNode := &eventlogger.JSONFormatter{}
err = e.broker.RegisterNode(jsonfmtId, fmtNode)
if err != nil {
return nil, errors.Wrap(err, "failed to register json node")
}
for _, s := range c.Sinks {
var sinkId eventlogger.NodeID
var sinkNode eventlogger.Node
switch s.SinkType {
case StdoutSink:
sinkNode = &eventlogger.WriterSink{
Format: string(s.Format),
Writer: os.Stdout,
}
id, err = newId("stdout")
if err != nil {
return nil, errors.Wrap(err, op)
}
sinkId = eventlogger.NodeID(id)
default:
sinkNode = &eventlogger.FileSink{
Format: string(s.Format),
Path: s.Path,
FileName: s.FileName,
MaxBytes: s.RotateBytes,
MaxDuration: s.RotateDuration,
MaxFiles: s.RotateMaxFiles,
}
id, err = newId(fmt.Sprintf("file_%s_%s_", s.Path, s.FileName))
if err != nil {
return nil, errors.Wrap(err, op)
}
sinkId = eventlogger.NodeID(id)
}
err = e.broker.RegisterNode(sinkId, sinkNode)
if err != nil {
return nil, errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("failed to register sink node %s", sinkId)))
}
var addToAudit, addToObservation, addToErr bool
for _, t := range s.EventTypes {
switch t {
case EveryType:
addToAudit = true
addToObservation = true
addToErr = true
case ErrorType:
addToErr = true
case AuditType:
addToAudit = true
case ObservationType:
addToObservation = true
}
}
if addToAudit {
auditPipelines = append(auditPipelines, pipeline{
eventType: AuditType,
fmtId: jsonfmtId,
sinkId: sinkId,
})
}
if addToObservation {
observationPipelines = append(observationPipelines, pipeline{
eventType: ObservationType,
fmtId: jsonfmtId,
sinkId: sinkId,
})
}
if addToErr {
errPipelines = append(errPipelines, pipeline{
eventType: ErrorType,
fmtId: jsonfmtId,
sinkId: sinkId,
})
}
}
if c.AuditEnabled && len(auditPipelines) == 0 {
return nil, errors.New(errors.InvalidParameter, op, "audit events enabled but no sink defined for it")
}
if c.ObservationsEnabled && len(observationPipelines) == 0 {
return nil, errors.New(errors.InvalidParameter, op, "observation events enabled but no sink defined for it")
}
auditNodeIds := make([]eventlogger.NodeID, 0, len(auditPipelines))
for _, p := range auditPipelines {
gatedFilterNode := eventlogger.GatedFilter{}
e.flushableNodes = append(e.flushableNodes, &gatedFilterNode)
gateId, err := newId("gated-audit")
if err != nil {
return nil, errors.Wrap(err, op)
}
gatedFilterNodeId := eventlogger.NodeID(gateId)
if err := e.broker.RegisterNode(gatedFilterNodeId, &gatedFilterNode); err != nil {
return nil, errors.Wrap(err, op, errors.WithMsg("unable to register audit gated filter"))
}
pipeId, err := newId(auditPipeline)
if err != nil {
return nil, errors.Wrap(err, op)
}
err = e.broker.RegisterPipeline(eventlogger.Pipeline{
EventType: eventlogger.EventType(p.eventType),
PipelineID: eventlogger.PipelineID(pipeId),
NodeIDs: []eventlogger.NodeID{gatedFilterNodeId, p.fmtId, p.sinkId},
})
if err != nil {
return nil, errors.Wrap(err, "failed to register audit pipeline")
}
auditNodeIds = append(auditNodeIds, p.sinkId)
}
observationNodeIds := make([]eventlogger.NodeID, 0, len(observationPipelines))
for _, p := range observationPipelines {
gatedFilterNode := eventlogger.GatedFilter{}
e.flushableNodes = append(e.flushableNodes, &gatedFilterNode)
gateId, err := newId("gated-observation")
if err != nil {
return nil, errors.Wrap(err, op)
}
gatedFilterNodeId := eventlogger.NodeID(gateId)
if err := e.broker.RegisterNode(gatedFilterNodeId, &gatedFilterNode); err != nil {
return nil, errors.Wrap(err, op, errors.WithMsg("unable to register audit gated filter"))
}
pipeId, err := newId(observationPipeline)
if err != nil {
return nil, errors.Wrap(err, op)
}
err = e.broker.RegisterPipeline(eventlogger.Pipeline{
EventType: eventlogger.EventType(p.eventType),
PipelineID: eventlogger.PipelineID(pipeId),
NodeIDs: []eventlogger.NodeID{gatedFilterNodeId, p.fmtId, p.sinkId},
})
if err != nil {
return nil, errors.Wrap(err, "failed to register observation pipeline")
}
observationNodeIds = append(observationNodeIds, p.sinkId)
}
errNodeIds := make([]eventlogger.NodeID, 0, len(errPipelines))
for _, p := range errPipelines {
pipeId, err := newId(errPipeline)
if err != nil {
return nil, errors.Wrap(err, op)
}
err = e.broker.RegisterPipeline(eventlogger.Pipeline{
EventType: eventlogger.EventType(p.eventType),
PipelineID: eventlogger.PipelineID(pipeId),
NodeIDs: []eventlogger.NodeID{p.fmtId, p.sinkId},
})
if err != nil {
return nil, errors.Wrap(err, "failed to register err pipeline")
}
errNodeIds = append(errNodeIds, p.sinkId)
}
// TODO(jimlambrt) go-eventlogger SetSuccessThreshold currently does not
// specify which sink passed and which hasn't so we are unable to
// support multiple sinks with different delivery guarantees
if c.AuditDelivery == Enforced {
err = e.broker.SetSuccessThreshold(eventlogger.EventType(AuditType), len(auditNodeIds))
if err != nil {
return nil, errors.Wrap(err, "failed to set success threshold for audit events")
}
}
if c.ObservationDelivery == Enforced {
err = e.broker.SetSuccessThreshold(eventlogger.EventType(ObservationType), len(observationNodeIds))
if err != nil {
return nil, errors.Wrap(err, "failed to set success threshold for observation events")
}
}
// always enforce delivery of errors
err = e.broker.SetSuccessThreshold(eventlogger.EventType(ErrorType), len(errNodeIds))
if err != nil {
return nil, errors.Wrap(err, "failed to set success threshold for error events")
}
return e, nil
}
// writeObservation writes/sends an Observation event.
func (e *Eventer) writeObservation(ctx context.Context, event *observation) error {
const op = "event.(Eventer).writeObservation"
if event == nil {
return errors.New(errors.InvalidParameter, op, "missing event")
}
if !e.conf.ObservationsEnabled {
return nil
}
err := e.retrySend(ctx, stdRetryCount, expBackoff{}, func() (eventlogger.Status, error) {
if event.Header != nil {
event.Header[RequestInfoField] = event.RequestInfo
event.Header[VersionField] = event.Version
}
if event.Detail != nil {
event.Detail[OpField] = string(event.Op)
}
return e.broker.Send(ctx, eventlogger.EventType(ObservationType), event.SimpleGatedPayload)
})
if err != nil {
e.logger.Error("encountered an error sending an observation event", "error:", err.Error())
return errors.Wrap(err, op)
}
return nil
}
// writeError writes/sends an Err event
func (e *Eventer) writeError(ctx context.Context, event *err) error {
const op = "event.(Eventer).writeError"
if event == nil {
return errors.New(errors.InvalidParameter, op, "missing event")
}
err := e.retrySend(ctx, stdRetryCount, expBackoff{}, func() (eventlogger.Status, error) {
return e.broker.Send(ctx, eventlogger.EventType(ErrorType), event)
})
if err != nil {
e.logger.Error("encountered an error sending an error event", "error:", err.Error())
return errors.Wrap(err, op)
}
return nil
}
// writeAudit writes/send an audit event
func (e *Eventer) writeAudit(ctx context.Context, event *audit) error {
const op = "event.(Eventer).writeAudit"
if event == nil {
return errors.New(errors.InvalidParameter, op, "missing event")
}
if !e.conf.AuditEnabled {
return nil
}
err := e.retrySend(ctx, stdRetryCount, expBackoff{}, func() (eventlogger.Status, error) {
return e.broker.Send(ctx, eventlogger.EventType(AuditType), event)
})
if err != nil {
e.logger.Error("encountered an error sending an audit event", "error:", err.Error())
return errors.Wrap(err, op)
}
return nil
}
// Reopen can used during a SIGHUP to reopen nodes, most importantly the underlying
// file sinks.
func (e *Eventer) Reopen() error {
if e.broker != nil {
return e.broker.Reopen(context.Background())
}
return nil
}
// FlushNodes will flush any of the eventer's flushable nodes. This
// needs to be called whenever Boundary is stopping (aka shutting down).
func (e *Eventer) FlushNodes(ctx context.Context) error {
const op = "event.(Eventer).FlushNodes"
for _, n := range e.flushableNodes {
if err := n.FlushAll(ctx); err != nil {
return errors.Wrap(err, op)
}
}
return nil
}

@ -0,0 +1,35 @@
package event
import (
"fmt"
"github.com/hashicorp/boundary/internal/errors"
)
// EventerConfig supplies all the configuration needed to create/config an Eventer.
type EventerConfig struct {
AuditDelivery DeliveryGuarantee // AuditDelivery specifies the delivery guarantees for audit events (enforced or best effort).
ObservationDelivery DeliveryGuarantee // ObservationDelivery specifies the delivery guarantees for observation events (enforced or best effort).
AuditEnabled bool // AuditEnabled specifies if audit events should be emitted.
ObservationsEnabled bool // ObservationsEnabled specifies if observation events should be emitted.
Sinks []SinkConfig // Sinks are all the configured sinks
}
// validate will validate the config. BTW, a config isn't required to have any
// sinks to be valid.
func (c *EventerConfig) validate() error {
const op = "event.(EventerConfig).validate"
if err := c.AuditDelivery.validate(); err != nil {
return errors.Wrap(err, op)
}
if err := c.ObservationDelivery.validate(); err != nil {
return errors.Wrap(err, op)
}
for i, s := range c.Sinks {
if err := s.validate(); err != nil {
return errors.Wrap(err, op, errors.WithMsg(fmt.Sprintf("sink %d is invalid", i)))
}
}
return nil
}

@ -0,0 +1,79 @@
package event
import (
"testing"
"github.com/hashicorp/boundary/internal/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEventerConfig_validate(t *testing.T) {
tests := []struct {
name string
c EventerConfig
wantErrMatch *errors.Template
wantErrContains string
}{
{
name: "invalid-audit-delivery",
c: EventerConfig{
AuditDelivery: "invalid",
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "is not a valid delivery guarantee",
},
{
name: "valid-audit-delivery",
c: EventerConfig{
AuditDelivery: Enforced,
},
},
{
name: "invalid-observation-delivery",
c: EventerConfig{
ObservationDelivery: "invalid",
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "is not a valid delivery guarantee",
},
{
name: "valid-observation-delivery",
c: EventerConfig{
ObservationDelivery: Enforced,
},
},
{
name: "invalid-sink",
c: EventerConfig{
Sinks: []SinkConfig{
{
SinkType: "invalid",
},
},
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "is not a valid sink type",
},
{
name: "valid-with-all-defaults",
c: EventerConfig{},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
err := tt.c.validate()
if tt.wantErrMatch != nil {
require.Error(err)
assert.Truef(errors.Match(tt.wantErrMatch, err), "wanted %q and got %q", tt.wantErrMatch, err)
if tt.wantErrContains != "" {
assert.Contains(err.Error(), tt.wantErrContains)
}
return
}
assert.NoError(err)
})
}
}

@ -0,0 +1,82 @@
package event
import (
"context"
"fmt"
"math"
"math/rand"
"time"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/eventlogger"
"github.com/hashicorp/go-multierror"
)
const (
// stdRetryCount is the standard number of times for retry when sending events
stdRetryCount = 3
)
type backoff interface {
duration(attemptNumber uint) time.Duration
}
type expBackoff struct{}
// duration returns an exponential backing off time duration
func (b expBackoff) duration(attempt uint) time.Duration {
r := rand.Float64()
return time.Millisecond * time.Duration(math.Exp2(float64(attempt))*5*(r+0.5))
}
type retryInfo struct {
retries int
backoff time.Duration
}
type sendHandler func() (eventlogger.Status, error)
// retrySend will attempt sendHandler (which is intended to be a closure that
// sends an event) the specified number of retries using the specified backoff.
func (e *Eventer) retrySend(ctx context.Context, retries uint, backOff backoff, handler sendHandler) error {
const op = "event.(Eventer).retrySend"
if backOff == nil {
return errors.New(errors.InvalidParameter, op, "missing backoff")
}
if handler == nil {
return errors.New(errors.InvalidParameter, op, "missing handler")
}
success := false
var retryErrors error
var attemptStatus eventlogger.Status
info := retryInfo{}
for attempts := uint(1); ; attempts++ {
if attempts > retries+1 {
retryErrors = multierror.Append(retryErrors, errors.New(errors.MaxRetries, op, fmt.Sprintf("Too many retries: reached max of %d", retries)))
return retryErrors
}
var err error
attemptStatus, err = handler()
if len(attemptStatus.Warnings) > 0 {
var retryWarnings error
for _, w := range attemptStatus.Warnings {
retryWarnings = multierror.Append(retryWarnings, w)
}
e.logger.Error("unable to send event", "operation", op, "warning", retryWarnings)
}
if err != nil {
retryErrors = multierror.Append(retryErrors, errors.Wrap(err, op))
d := backOff.duration(attempts)
info.retries++
info.backoff = info.backoff + d
time.Sleep(d)
continue
}
success = true
break
}
if !success {
return errors.Wrap(retryErrors, op, errors.WithMsg("failed to send event"))
}
return nil
}

@ -0,0 +1,116 @@
package event
import (
"context"
"os"
"testing"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/eventlogger"
"github.com/hashicorp/go-hclog"
"github.com/hashicorp/go-multierror"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestEventer_retrySend(t *testing.T) {
t.Parallel()
ctx := context.Background()
testConfig := TestEventerConfig(t, "TestEventer_retrySend")
eventer, err := NewEventer(hclog.Default(), testConfig.EventerConfig)
require.NoError(t, err)
testError := errors.New(errors.InvalidParameter, "missing-operation", "missing operation")
testEvent, err := newError("TestEventer_retrySend", testError, WithId("test-error"))
require.NoError(t, err)
tests := []struct {
name string
retries uint
backOff backoff
handler sendHandler
wantErrMatch *errors.Template
wantErrContain string
}{
{
name: "missing-backoff",
retries: 1,
handler: func() (eventlogger.Status, error) {
return eventer.broker.Send(ctx, eventlogger.EventType(ErrorType), testEvent)
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContain: "missing backoff",
},
{
name: "missing-handler",
retries: 1,
backOff: expBackoff{},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContain: "missing handler",
},
{
name: "too-many-retries",
retries: 3,
backOff: expBackoff{},
handler: func() (eventlogger.Status, error) {
return eventlogger.Status{}, errors.New(errors.InvalidParameter, "TestEventer_retrySend", "will never work")
},
wantErrMatch: errors.T(errors.MaxRetries),
wantErrContain: "Too many retries",
},
{
name: "success-with-warnings",
retries: 3,
backOff: expBackoff{},
handler: func() (eventlogger.Status, error) {
return eventlogger.Status{
Warnings: []error{errors.New(errors.RecordNotFound, "TestEventer_retrySend", "not found")},
}, nil
},
},
{
name: "success",
retries: 1,
backOff: expBackoff{},
handler: func() (eventlogger.Status, error) {
return eventlogger.Status{}, nil
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
defer os.Remove(testConfig.AllEvents.Name())
defer os.Remove(testConfig.ErrorEvents.Name())
err := eventer.retrySend(ctx, tt.retries, tt.backOff, tt.handler)
if tt.wantErrMatch != nil {
require.Error(err)
multi, isMultiError := err.(*multierror.Error)
switch isMultiError {
case true:
matched := false
for _, e := range multi.WrappedErrors() {
if errors.Match(tt.wantErrMatch, e) {
if tt.wantErrContain != "" {
assert.Contains(err.Error(), tt.wantErrContain)
}
matched = true
}
}
assert.True(matched)
default:
assert.True(errors.Match(tt.wantErrMatch, err))
if tt.wantErrContain != "" {
assert.Contains(err.Error(), tt.wantErrContain)
}
}
return
}
require.NoError(err)
})
}
}

@ -0,0 +1,492 @@
package event
import (
"context"
"strings"
"testing"
"github.com/hashicorp/boundary/internal/errors"
"github.com/hashicorp/eventlogger"
"github.com/hashicorp/go-hclog"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_InitSysEventer(t *testing.T) {
// this test and its subtests cannot be run in parallel because of it's
// dependency on the sysEventer
testConfig := TestEventerConfig(t, "InitSysEventer")
tests := []struct {
name string
log hclog.Logger
config EventerConfig
want *Eventer
wantErrMatch *errors.Template
}{
{
name: "missing-hclog",
config: testConfig.EventerConfig,
wantErrMatch: errors.T(errors.InvalidParameter),
},
{
name: "success",
config: testConfig.EventerConfig,
log: hclog.Default(),
want: &Eventer{
logger: hclog.Default(),
conf: testConfig.EventerConfig,
},
},
{
name: "success-with-default-config",
config: EventerConfig{},
log: hclog.Default(),
want: &Eventer{
logger: hclog.Default(),
conf: EventerConfig{
Sinks: []SinkConfig{
{
Name: "default",
EventTypes: []Type{EveryType},
Format: JSONSinkFormat,
SinkType: StdoutSink,
},
},
},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
defer TestResetSystEventer(t)
assert, require := assert.New(t), require.New(t)
err := InitSysEventer(tt.log, tt.config)
got := SysEventer()
if tt.wantErrMatch != nil {
require.Nil(got)
require.Error(err)
if tt.wantErrMatch != nil {
assert.True(errors.Match(tt.wantErrMatch, err))
}
return
}
require.NoError(err)
require.NotNil(got)
tt.want.broker = got.broker
tt.want.flushableNodes = got.flushableNodes
assert.Equal(tt.want, got)
})
}
}
func TestEventer_writeObservation(t *testing.T) {
t.Parallel()
ctx := context.Background()
testSetup := TestEventerConfig(t, "TestEventer_writeObservation")
eventer, err := NewEventer(hclog.Default(), testSetup.EventerConfig)
require.NoError(t, err)
testHeader := map[string]interface{}{"name": "header"}
testDetail := map[string]interface{}{"name": "details"}
testObservation, err := newObservation("Test_NewEventer", WithHeader(testHeader), WithDetails(testDetail))
require.NoError(t, err)
tests := []struct {
name string
broker broker
observation *observation
wantErrMatch *errors.Template
}{
{
name: "missing-observation",
broker: &testMockBroker{},
wantErrMatch: errors.T(errors.InvalidParameter),
},
{
name: "send-fails",
broker: &testMockBroker{errorOnSend: errors.New(errors.Io, "test", "no msg")},
observation: testObservation,
wantErrMatch: errors.T(errors.Io),
},
{
name: "success",
broker: &testMockBroker{},
observation: testObservation,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
eventer.broker = tt.broker
err = eventer.writeObservation(ctx, tt.observation)
if tt.wantErrMatch != nil {
require.Error(err)
assert.Truef(errors.Match(tt.wantErrMatch, err), "got %q and wanted %q", err.Error(), tt.wantErrMatch)
return
}
require.NoError(err)
})
}
t.Run("e2e", func(t *testing.T) {
require := require.New(t)
logger := hclog.New(&hclog.LoggerOptions{
Name: "test",
})
c := EventerConfig{
ObservationsEnabled: true,
}
// with no defined config, it will default to a stdout sink
e, err := NewEventer(logger, c)
require.NoError(err)
m := map[string]interface{}{
"name": "bar",
"list": []string{"1", "2"},
}
observationEvent, err := newObservation("Test_NewEventer", WithHeader(m))
require.NoError(err)
require.NoError(e.writeObservation(context.Background(), observationEvent))
})
}
func TestEventer_writeAudit(t *testing.T) {
t.Parallel()
ctx := context.Background()
testSetup := TestEventerConfig(t, "Test_NewEventer")
eventer, err := NewEventer(hclog.Default(), testSetup.EventerConfig)
require.NoError(t, err)
testAudit, err := newAudit(
"TestEventer_writeAudit",
WithRequestInfo(TestRequestInfo(t)),
WithAuth(testAuth(t)),
WithRequest(testRequest(t)),
WithResponse(testResponse(t)))
require.NoError(t, err)
tests := []struct {
name string
broker broker
audit *audit
wantErrMatch *errors.Template
}{
{
name: "missing-audit",
broker: &testMockBroker{},
wantErrMatch: errors.T(errors.InvalidParameter),
},
{
name: "send-fails",
broker: &testMockBroker{errorOnSend: errors.New(errors.Io, "test", "no msg")},
audit: testAudit,
wantErrMatch: errors.T(errors.Io),
},
{
name: "success",
broker: &testMockBroker{},
audit: testAudit,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
eventer.broker = tt.broker
err = eventer.writeAudit(ctx, tt.audit)
if tt.wantErrMatch != nil {
require.Error(err)
assert.Truef(errors.Match(tt.wantErrMatch, err), "got %q and wanted %q", err.Error(), tt.wantErrMatch)
return
}
require.NoError(err)
})
}
}
func TestEventer_writeError(t *testing.T) {
t.Parallel()
ctx := context.Background()
testSetup := TestEventerConfig(t, "Test_NewEventer")
eventer, er := NewEventer(hclog.Default(), testSetup.EventerConfig)
require.NoError(t, er)
testError, er := newError("TestEventer_writeError", errors.New(errors.Io, "test", "no msg"))
require.NoError(t, er)
tests := []struct {
name string
broker broker
err *err
wantErrMatch *errors.Template
}{
{
name: "missing-error",
broker: &testMockBroker{},
wantErrMatch: errors.T(errors.InvalidParameter),
},
{
name: "send-fails",
broker: &testMockBroker{errorOnSend: errors.New(errors.Io, "test", "no msg")},
err: testError,
wantErrMatch: errors.T(errors.Io),
},
{
name: "success",
broker: &testMockBroker{},
err: testError,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
eventer.broker = tt.broker
err := eventer.writeError(ctx, tt.err)
if tt.wantErrMatch != nil {
require.Error(err)
assert.Truef(errors.Match(tt.wantErrMatch, err), "got %q and wanted %q", err.Error(), tt.wantErrMatch)
return
}
require.NoError(err)
})
}
}
func Test_NewEventer(t *testing.T) {
t.Parallel()
testSetup := TestEventerConfig(t, "Test_NewEventer")
testSetupWithOpts := TestEventerConfig(t, "Test_NewEventer", testWithAuditSink(), testWithObservationSink())
tests := []struct {
name string
config EventerConfig
opts []Option
logger hclog.Logger
want *Eventer
wantRegistered []string
wantPipelines []string
wantThresholds map[eventlogger.EventType]int
wantErrMatch *errors.Template
}{
{
name: "invalid-config",
config: EventerConfig{
AuditDelivery: "invalid",
},
logger: hclog.Default(),
wantErrMatch: errors.T(errors.InvalidParameter),
},
{
name: "missing-logger",
config: testSetup.EventerConfig,
wantErrMatch: errors.T(errors.InvalidParameter),
},
{
name: "success-with-default-config",
config: EventerConfig{},
logger: hclog.Default(),
want: &Eventer{
logger: hclog.Default(),
conf: EventerConfig{
Sinks: []SinkConfig{
{
Name: "default",
EventTypes: []Type{EveryType},
Format: JSONSinkFormat,
SinkType: StdoutSink,
},
},
},
},
wantRegistered: []string{
"json", // fmt for everything
"stdout", // stdout
"gated-observation", // stdout
"gated-audit", // stdout
},
wantPipelines: []string{
"audit", // stdout
"observation", // stdout
"error", // stdout
},
wantThresholds: map[eventlogger.EventType]int{
"error": 1,
},
},
{
name: "testSetup",
config: testSetup.EventerConfig,
logger: hclog.Default(),
want: &Eventer{
logger: hclog.Default(),
conf: testSetup.EventerConfig,
},
wantRegistered: []string{
"json", // fmt for everything
"stdout", // stdout
"gated-observation", // stdout
"gated-audit", // stdout
"tmp-all-events", // every-type-file-sync
"gated-observation", // every-type-file-sync
"gated-audit", // every-type-file-sync
"tmp-errors", // error-file-sink
},
wantPipelines: []string{
"audit", // every-type-file-sync
"audit", // stdout
"observation", // every-type-file-sync
"observation", // stdout
"error", // every-type-file-sync
"error", // stdout
"error", // error-file-sink
},
wantThresholds: map[eventlogger.EventType]int{
"audit": 2,
"error": 3,
"observation": 2,
},
},
{
name: "testSetup-with-all-opts",
config: testSetupWithOpts.EventerConfig,
logger: hclog.Default(),
want: &Eventer{
logger: hclog.Default(),
conf: testSetupWithOpts.EventerConfig,
},
wantRegistered: []string{
"json", // fmt for everything
"stdout", // stdout
"gated-observation", // stdout
"gated-audit", // stdout
"tmp-all-events", // every-type-file-sync
"gated-observation", // every-type-file-sync
"gated-audit", // every-type-file-sync
"tmp-errors", // error-file-sink
"gated-observation", // observation-file-sink
"tmp-observation", // observations-file-sink
"gated-audit", // audit-file-sink
"tmp-audit", // audit-file-sink
},
wantPipelines: []string{
"audit", // every-type-file-sync
"audit", // stdout
"observation", // every-type-file-sync
"observation", // stdout
"error", // every-type-file-sync
"error", // stdout
"error", // error-file-sink
"audit", // audit-file-sink
"observation", // observation-file-sink
},
wantThresholds: map[eventlogger.EventType]int{
"audit": 3,
"error": 3,
"observation": 3,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
testBroker := &testMockBroker{}
got, err := NewEventer(tt.logger, tt.config, testWithBroker(testBroker))
if tt.wantErrMatch != nil {
require.Error(err)
require.Nil(got)
assert.True(errors.Match(tt.wantErrMatch, err))
return
}
require.NoError(err)
require.NotNil(got)
tt.want.broker = got.broker
tt.want.flushableNodes = got.flushableNodes
assert.Equal(tt.want, got)
assert.Lenf(testBroker.registeredNodeIds, len(tt.wantRegistered), "got nodes: %q", testBroker.registeredNodeIds)
for _, want := range tt.wantRegistered {
found := false
for _, got := range testBroker.registeredNodeIds {
if strings.Contains(string(got), want) {
found = true
break
}
}
assert.Truef(found, "did not find %s in the registered nodes: %s", want, testBroker.registeredNodeIds)
}
assert.Lenf(testBroker.pipelines, len(tt.wantPipelines), "got pipelines: %q", testBroker.pipelines)
for _, want := range tt.wantPipelines {
found := false
for _, got := range testBroker.pipelines {
if strings.Contains(string(got.EventType), want) {
found = true
break
}
}
assert.Truef(found, "did not find %s in the registered pipelines: %s", want, testBroker.pipelines)
}
assert.Equal(tt.wantThresholds, testBroker.successThresholds)
})
}
}
func TestEventer_Reopen(t *testing.T) {
t.Parallel()
t.Run("simple", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
e, err := NewEventer(hclog.Default(), EventerConfig{})
require.NoError(err)
e.broker = nil
require.NoError(e.Reopen())
e.broker = &testMockBroker{}
require.NoError(e.Reopen())
assert.True(e.broker.(*testMockBroker).reopened)
})
}
func TestEventer_FlushNodes(t *testing.T) {
t.Parallel()
t.Run("simple", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
e, err := NewEventer(hclog.Default(), EventerConfig{})
require.NoError(err)
node := &testFlushNode{}
e.flushableNodes = append(e.flushableNodes, node)
require.NoError(e.FlushNodes(context.Background()))
node.raiseError = true
require.Error(e.FlushNodes(context.Background()))
assert.True(node.flushed)
})
}
type testFlushNode struct {
flushed bool
raiseError bool
}
func (t *testFlushNode) FlushAll(_ context.Context) error {
t.flushed = true
if t.raiseError {
return errors.New(errors.InvalidParameter, "flush-all", "test error")
}
return nil
}

@ -0,0 +1,15 @@
package event
import (
"github.com/hashicorp/boundary/internal/db"
"github.com/hashicorp/boundary/internal/errors"
)
func newId(prefix string) (string, error) {
const op = "event.newId"
id, err := db.NewPublicId(prefix)
if err != nil {
return "", errors.Wrap(err, op)
}
return id, nil
}

@ -0,0 +1,18 @@
package event
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_newId(t *testing.T) {
t.Run("basics", func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
got, err := newId("pre")
require.NoError(err)
assert.True(strings.HasPrefix(got, "pre"))
})
}

@ -0,0 +1,101 @@
package event
import (
"time"
)
// getOpts - iterate the inbound Options and return a struct.
func getOpts(opt ...Option) options {
opts := getDefaultOptions()
for _, o := range opt {
o(&opts)
}
return opts
}
// Option - how Options are passed as arguments.
type Option func(*options)
// options = how options are represented
type options struct {
withId string
withDetails map[string]interface{}
withHeader map[string]interface{}
withFlush bool
withRequestInfo *RequestInfo
withNow time.Time
withRequest *Request
withResponse *Response
withAuth *Auth
withBroker broker // test only option
withAuditSink bool // test only option
withObservationSink bool // test only option
}
func getDefaultOptions() options {
return options{}
}
// WithId allows an optional Id
func WithId(id string) Option {
return func(o *options) {
o.withId = id
}
}
// WithDetails allows an optional map as details
func WithDetails(d map[string]interface{}) Option {
return func(o *options) {
o.withDetails = d
}
}
// WithHeader allows an optional map as a header
func WithHeader(d map[string]interface{}) Option {
return func(o *options) {
o.withHeader = d
}
}
// WithFlush allows an optional flush option.
func WithFlush() Option {
return func(o *options) {
o.withFlush = true
}
}
// WithRequestInfo allows an optional RequestInfo
func WithRequestInfo(i *RequestInfo) Option {
return func(o *options) {
o.withRequestInfo = i
}
}
// WithNow allows an option time.Time to represent now.
func WithNow(now time.Time) Option {
return func(o *options) {
o.withNow = now
}
}
// WithRequest allows an optional request
func WithRequest(r *Request) Option {
return func(o *options) {
o.withRequest = r
}
}
// WithResponse allows an optional response
func WithResponse(r *Response) Option {
return func(o *options) {
o.withResponse = r
}
}
// WithAuth allows an optional Auth
func WithAuth(a *Auth) Option {
return func(o *options) {
o.withAuth = a
}
}

@ -0,0 +1,87 @@
package event
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
)
// Test_GetOpts provides unit tests for GetOpts and all the options
func Test_GetOpts(t *testing.T) {
t.Parallel()
t.Run("WithId", func(t *testing.T) {
assert := assert.New(t)
opts := getOpts(WithId("test"))
testOpts := getDefaultOptions()
testOpts.withId = "test"
assert.Equal(opts, testOpts)
})
t.Run("WithDetails", func(t *testing.T) {
assert := assert.New(t)
d := map[string]interface{}{
"name": "alice",
}
opts := getOpts(WithDetails(d))
testOpts := getDefaultOptions()
testOpts.withDetails = d
assert.Equal(opts, testOpts)
})
t.Run("WithHeader", func(t *testing.T) {
assert := assert.New(t)
h := map[string]interface{}{
"name": "alice",
}
opts := getOpts(WithHeader(h))
testOpts := getDefaultOptions()
testOpts.withHeader = h
assert.Equal(opts, testOpts)
})
t.Run("WithFlush", func(t *testing.T) {
assert := assert.New(t)
opts := getOpts(WithFlush())
testOpts := getDefaultOptions()
testOpts.withFlush = true
assert.Equal(opts, testOpts)
})
t.Run("WithRequestInfo", func(t *testing.T) {
assert := assert.New(t)
info := TestRequestInfo(t)
opts := getOpts(WithRequestInfo(info))
testOpts := getDefaultOptions()
testOpts.withRequestInfo = info
assert.Equal(opts, testOpts)
})
t.Run("WithNow", func(t *testing.T) {
assert := assert.New(t)
now := time.Now()
opts := getOpts(WithNow(now))
testOpts := getDefaultOptions()
testOpts.withNow = now
assert.Equal(opts, testOpts)
})
t.Run("WithRequest", func(t *testing.T) {
assert := assert.New(t)
r := testRequest(t)
opts := getOpts(WithRequest(r))
testOpts := getDefaultOptions()
testOpts.withRequest = r
assert.Equal(opts, testOpts)
})
t.Run("WithResponse", func(t *testing.T) {
assert := assert.New(t)
r := testResponse(t)
opts := getOpts(WithResponse(r))
testOpts := getDefaultOptions()
testOpts.withResponse = r
assert.Equal(opts, testOpts)
})
t.Run("WithAuth", func(t *testing.T) {
assert := assert.New(t)
auth := testAuth(t)
opts := getOpts(WithAuth(auth))
testOpts := getDefaultOptions()
testOpts.withAuth = auth
assert.Equal(opts, testOpts)
})
}

@ -0,0 +1,45 @@
package event
import (
"time"
"github.com/hashicorp/boundary/internal/errors"
)
// SinkConfig defines the configuration for a Eventer sink
type SinkConfig struct {
Name string // Name defines a name for the sink.
EventTypes []Type // EventTypes defines a list of event types that will be sent to the sink. See the docs for EventTypes for a list of accepted values.
SinkType SinkType // SinkType defines the type of sink (StdoutSink or FileSink)
Format SinkFormat // Format defines the format for the sink (JSONSinkFormat)
Path string // Path defines the file path for the sink
FileName string // FileName defines the file name for the sink
RotateBytes int // RotateByes defines the number of bytes that should trigger rotation of a FileSink
RotateDuration time.Duration // RotateDuration defines how often a FileSink should be rotated
RotateMaxFiles int // RotateMaxFiles defines how may historical rotated files should be kept for a FileSink
}
func (sc *SinkConfig) validate() error {
const op = "event.(SinkConfig).validate"
if err := sc.SinkType.validate(); err != nil {
return errors.Wrap(err, op)
}
if err := sc.Format.validate(); err != nil {
return errors.Wrap(err, op)
}
if sc.SinkType == FileSink && sc.FileName == "" {
return errors.New(errors.InvalidParameter, op, "missing sink file name")
}
if sc.Name == "" {
return errors.New(errors.InvalidParameter, op, "missing sink name")
}
if len(sc.EventTypes) == 0 {
return errors.New(errors.InvalidParameter, op, "missing event types")
}
for _, et := range sc.EventTypes {
if err := et.validate(); err != nil {
return errors.Wrap(err, op)
}
}
return nil
}

@ -0,0 +1,135 @@
package event
import (
"testing"
"github.com/hashicorp/boundary/internal/errors"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestSinkConfig_validate(t *testing.T) {
t.Parallel()
tests := []struct {
name string
sc SinkConfig
wantErrMatch *errors.Template
wantErrContains string
}{
{
name: "missing-name",
sc: SinkConfig{
EventTypes: []Type{EveryType},
SinkType: FileSink,
FileName: "tmp.file",
Format: JSONSinkFormat,
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing sink name",
},
{
name: "missing-EventType",
sc: SinkConfig{
Name: "sink-name",
SinkType: FileSink,
FileName: "tmp.file",
Format: JSONSinkFormat,
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing event types",
},
{
name: "invalid-EventType",
sc: SinkConfig{
Name: "sink-name",
EventTypes: []Type{"invalid"},
SinkType: FileSink,
FileName: "tmp.file",
Format: JSONSinkFormat,
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "not a valid event type",
},
{
name: "missing-sink-type",
sc: SinkConfig{
Name: "sink-name",
EventTypes: []Type{EveryType},
FileName: "tmp.file",
Format: JSONSinkFormat,
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "not a valid sink type",
},
{
name: "invalid-sink-type",
sc: SinkConfig{
Name: "sink-name",
EventTypes: []Type{EveryType},
SinkType: "invalid",
FileName: "tmp.file",
Format: JSONSinkFormat,
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "not a valid sink type",
},
{
name: "missing-format",
sc: SinkConfig{
Name: "sink-name",
SinkType: FileSink,
EventTypes: []Type{EveryType},
FileName: "tmp.file",
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "not a valid sink format",
},
{
name: "invalid-format",
sc: SinkConfig{
Name: "sink-name",
Format: "invalid",
SinkType: FileSink,
EventTypes: []Type{EveryType},
FileName: "tmp.file",
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "not a valid sink format",
},
{
name: "file-sink-with-no-file-name",
sc: SinkConfig{
EventTypes: []Type{EveryType},
SinkType: FileSink,
Format: JSONSinkFormat,
},
wantErrMatch: errors.T(errors.InvalidParameter),
wantErrContains: "missing sink file name",
},
{
name: "valid",
sc: SinkConfig{
Name: "valid",
EventTypes: []Type{EveryType},
SinkType: FileSink,
FileName: "tmp.file",
Format: JSONSinkFormat,
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert, require := assert.New(t), require.New(t)
err := tt.sc.validate()
if tt.wantErrMatch != nil {
require.Error(err)
require.Truef(errors.Match(tt.wantErrMatch, err), "wanted %q and got %q", tt.wantErrMatch, err)
if tt.wantErrContains != "" {
assert.Contains(err.Error(), tt.wantErrContains)
}
return
}
assert.NoError(err)
})
}
}

@ -0,0 +1,23 @@
package event
import (
"fmt"
"github.com/hashicorp/boundary/internal/errors"
)
const (
JSONSinkFormat SinkFormat = "json" // JSONSinkFormat means the event is formatted as JSON
)
type SinkFormat string // SinkFormat defines the formatting for a sink in a config file stanza (json)
func (f SinkFormat) validate() error {
const op = "event.(SinkFormat).validate"
switch f {
case JSONSinkFormat:
return nil
default:
return errors.New(errors.InvalidParameter, op, fmt.Sprintf("'%s' is not a valid sink format", f))
}
}

@ -0,0 +1,24 @@
package event
import (
"fmt"
"github.com/hashicorp/boundary/internal/errors"
)
const (
StdoutSink SinkType = "stdout" // StdoutSink is written to stdout
FileSink SinkType = "file" // FileSink is written to a file
)
type SinkType string // SinkType defines the type of sink in a config stanza (file, stdout)
func (t SinkType) validate() error {
const op = "event.(SinkType).validate"
switch t {
case StdoutSink, FileSink:
return nil
default:
return errors.New(errors.InvalidParameter, op, fmt.Sprintf("'%s' is not a valid sink type", t))
}
}

@ -0,0 +1,230 @@
package event
import (
"context"
"io/ioutil"
"os"
"sync"
"testing"
"time"
"github.com/hashicorp/boundary/internal/gen/controller/api/resources/groups"
pbs "github.com/hashicorp/boundary/internal/gen/controller/api/services"
"github.com/hashicorp/eventlogger"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/types/known/wrapperspb"
)
var testSysEventerLock sync.Mutex
// TestResetSysEventer will reset event.syseventer to an uninitialized state.
func TestResetSystEventer(t *testing.T) {
t.Helper()
testSysEventerLock.Lock()
defer testSysEventerLock.Unlock()
sysEventerOnce = sync.Once{}
sysEventer = nil
}
type TestConfig struct {
EventerConfig EventerConfig
AllEvents *os.File
ErrorEvents *os.File
ObservationEvents *os.File
AuditEvents *os.File
}
// TestEventerConfig creates a test config and registers a cleanup func for its
// test tmp files.
func TestEventerConfig(t *testing.T, testName string, opt ...Option) TestConfig {
t.Helper()
require := require.New(t)
tmpAllFile, err := ioutil.TempFile("./", "tmp-all-events-"+testName)
require.NoError(err)
tmpErrFile, err := ioutil.TempFile("./", "tmp-errors-"+testName)
require.NoError(err)
t.Cleanup(func() {
os.Remove(tmpAllFile.Name())
os.Remove(tmpErrFile.Name())
})
c := TestConfig{
EventerConfig: EventerConfig{
ObservationsEnabled: true,
ObservationDelivery: Enforced,
AuditEnabled: true,
AuditDelivery: Enforced,
Sinks: []SinkConfig{
{
Name: "every-type-file-sink",
SinkType: FileSink,
EventTypes: []Type{EveryType},
Format: JSONSinkFormat,
Path: "./",
FileName: tmpAllFile.Name(),
},
{
Name: "stdout",
SinkType: StdoutSink,
EventTypes: []Type{EveryType},
Format: JSONSinkFormat,
},
{
Name: "err-file-sink",
SinkType: FileSink,
EventTypes: []Type{ErrorType},
Format: JSONSinkFormat,
Path: "./",
FileName: tmpErrFile.Name(),
},
},
},
AllEvents: tmpAllFile,
ErrorEvents: tmpErrFile,
}
opts := getOpts(opt...)
if opts.withAuditSink {
tmpFile, err := ioutil.TempFile("./", "tmp-audit-"+testName)
require.NoError(err)
t.Cleanup(func() {
os.Remove(tmpFile.Name())
})
c.EventerConfig.Sinks = append(c.EventerConfig.Sinks, SinkConfig{
Name: "audit-file-sink",
SinkType: FileSink,
EventTypes: []Type{AuditType},
Format: JSONSinkFormat,
Path: "./",
FileName: tmpFile.Name(),
})
}
if opts.withObservationSink {
tmpFile, err := ioutil.TempFile("./", "tmp-observation-"+testName)
require.NoError(err)
t.Cleanup(func() {
os.Remove(tmpFile.Name())
})
c.EventerConfig.Sinks = append(c.EventerConfig.Sinks, SinkConfig{
Name: "err-observation-sink",
SinkType: FileSink,
EventTypes: []Type{ObservationType},
Format: JSONSinkFormat,
Path: "./",
FileName: tmpFile.Name(),
})
}
return c
}
// TestRequestInfo provides a test RequestInfo
func TestRequestInfo(t *testing.T) *RequestInfo {
t.Helper()
return &RequestInfo{
Id: "test-request-info",
Method: "POST",
Path: "/test/request/info",
PublicId: "public-id",
}
}
func testAuth(t *testing.T) *Auth {
t.Helper()
return &Auth{
UserEmail: "test-auth@example.com",
UserName: "test-auth-user-name",
}
}
func testRequest(t *testing.T) *Request {
t.Helper()
return &Request{
Operation: "op",
Endpoint: "/group/<id>",
Details: &pbs.GetGroupRequest{
Id: "group-id",
},
}
}
func testResponse(t *testing.T) *Response {
t.Helper()
return &Response{
StatusCode: 200,
Details: &pbs.GetGroupResponse{
Item: &groups.Group{
Id: "group-id",
ScopeId: "org-id",
Name: &wrapperspb.StringValue{
Value: "group-name",
},
},
},
}
}
// testWithBroker is an unexported and a test option for passing in an optional broker
func testWithBroker(b broker) Option {
return func(o *options) {
o.withBroker = b
}
}
// testWithObservationSink is an unexported and a test option
func testWithObservationSink() Option {
return func(o *options) {
o.withObservationSink = true
}
}
// testWithAuditSink is an unexported and a test option
func testWithAuditSink() Option {
return func(o *options) {
o.withAuditSink = true
}
}
type testMockBroker struct {
reopened bool
stopTimeAt time.Time
registeredNodeIds []eventlogger.NodeID
successThresholds map[eventlogger.EventType]int
pipelines []eventlogger.Pipeline
errorOnSend error
}
func (b *testMockBroker) Reopen(ctx context.Context) error {
b.reopened = true
return nil
}
func (b *testMockBroker) RegisterPipeline(def eventlogger.Pipeline) error {
b.pipelines = append(b.pipelines, def)
return nil
}
func (b *testMockBroker) Send(ctx context.Context, t eventlogger.EventType, payload interface{}) (eventlogger.Status, error) {
if b.errorOnSend != nil {
return eventlogger.Status{}, b.errorOnSend
}
return eventlogger.Status{}, nil
}
func (b *testMockBroker) StopTimeAt(t time.Time) {
b.stopTimeAt = t
}
func (b *testMockBroker) RegisterNode(id eventlogger.NodeID, node eventlogger.Node) error {
b.registeredNodeIds = append(b.registeredNodeIds, id)
return nil
}
func (b *testMockBroker) SetSuccessThreshold(t eventlogger.EventType, successThreshold int) error {
if b.successThresholds == nil {
b.successThresholds = map[eventlogger.EventType]int{}
}
b.successThresholds[t] = successThreshold
return nil
}

@ -994,7 +994,7 @@ func validateUpdateRequest(req *pbs.UpdateAuthMethodRequest) error {
if err != nil {
badFields[issuerField] = fmt.Sprintf("Cannot be parsed as a url. %v", err)
}
if !strutil.StrListContains([]string{"http", "https"}, iss.Scheme) {
if iss != nil && !strutil.StrListContains([]string{"http", "https"}, iss.Scheme) {
badFields[issuerField] = fmt.Sprintf("Must have schema %q or %q specified", "http", "https")
}
}

@ -430,6 +430,25 @@ func TestUpdate_OIDC(t *testing.T) {
},
},
},
{
name: "invalid-issuer-port",
req: &pbs.UpdateAuthMethodRequest{
UpdateMask: &field_mask.FieldMask{
Paths: []string{"attributes.issuer"},
},
Item: &pb.AuthMethod{
Attributes: &structpb.Struct{
Fields: func() map[string]*structpb.Value {
f := defaultAttributeFields()
f["issuer"] = structpb.NewStringValue("http://localhost:7dddd")
f["disable_discovered_config_validation"] = structpb.NewBoolValue(true)
return f
}(),
},
},
},
err: handlers.ApiErrorWithCode(codes.InvalidArgument),
},
{
name: "No Update Mask",
req: &pbs.UpdateAuthMethodRequest{

@ -1,3 +1,3 @@
NEXT_PUBLIC_ALGOLIA_APP_ID=YY0FFNI7MF
NEXT_PUBLIC_ALGOLIA_INDEX=product_BOUNDARY
NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_API_KEY=5037da4824714676226913c65e961ca0
NEXT_PUBLIC_ALGOLIA_SEARCH_ONLY_API_KEY=8308498decdf72e11590fc6356e5fdde

Loading…
Cancel
Save