mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 10:37:56 +00:00 
			
		
		
		
	VAULT-24798: audit - improve error messages (#26312)
* audit: remove 'op' from error messages and do some clean up * Allow early error checking to be concerned with vault/Core vs. audit
This commit is contained in:
		| @@ -11,7 +11,6 @@ import ( | |||||||
| 	"github.com/hashicorp/eventlogger" | 	"github.com/hashicorp/eventlogger" | ||||||
| 	"github.com/hashicorp/go-bexpr" | 	"github.com/hashicorp/go-bexpr" | ||||||
| 	"github.com/hashicorp/vault/helper/namespace" | 	"github.com/hashicorp/vault/helper/namespace" | ||||||
| 	"github.com/hashicorp/vault/internal/observability/event" |  | ||||||
| 	"github.com/hashicorp/vault/sdk/logical" | 	"github.com/hashicorp/vault/sdk/logical" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -27,16 +26,14 @@ type EntryFilter struct { | |||||||
| // NewEntryFilter should be used to create an EntryFilter node. | // NewEntryFilter should be used to create an EntryFilter node. | ||||||
| // The filter supplied should be in bexpr format and reference fields from logical.LogInputBexpr. | // The filter supplied should be in bexpr format and reference fields from logical.LogInputBexpr. | ||||||
| func NewEntryFilter(filter string) (*EntryFilter, error) { | func NewEntryFilter(filter string) (*EntryFilter, error) { | ||||||
| 	const op = "audit.NewEntryFilter" |  | ||||||
|  |  | ||||||
| 	filter = strings.TrimSpace(filter) | 	filter = strings.TrimSpace(filter) | ||||||
| 	if filter == "" { | 	if filter == "" { | ||||||
| 		return nil, fmt.Errorf("%s: cannot create new audit filter with empty filter expression: %w", op, event.ErrInvalidParameter) | 		return nil, fmt.Errorf("cannot create new audit filter with empty filter expression: %w", ErrExternalOptions) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	eval, err := bexpr.CreateEvaluator(filter) | 	eval, err := bexpr.CreateEvaluator(filter) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: cannot create new audit filter: %w", op, err) | 		return nil, fmt.Errorf("cannot create new audit filter: %w: %w", ErrExternalOptions, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Validate the filter by attempting to evaluate it with an empty input. | 	// Validate the filter by attempting to evaluate it with an empty input. | ||||||
| @@ -45,7 +42,7 @@ func NewEntryFilter(filter string) (*EntryFilter, error) { | |||||||
| 	li := logical.LogInputBexpr{} | 	li := logical.LogInputBexpr{} | ||||||
| 	_, err = eval.Evaluate(li) | 	_, err = eval.Evaluate(li) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: filter references an unsupported field: %s", op, filter) | 		return nil, fmt.Errorf("filter references an unsupported field: %s: %w: %w", filter, ErrExternalOptions, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return &EntryFilter{evaluator: eval}, nil | 	return &EntryFilter{evaluator: eval}, nil | ||||||
| @@ -64,8 +61,6 @@ func (*EntryFilter) Type() eventlogger.NodeType { | |||||||
| // Process will attempt to parse the incoming event data and decide whether it | // Process will attempt to parse the incoming event data and decide whether it | ||||||
| // should be filtered or remain in the pipeline and passed to the next node. | // should be filtered or remain in the pipeline and passed to the next node. | ||||||
| func (f *EntryFilter) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) { | func (f *EntryFilter) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) { | ||||||
| 	const op = "audit.(EntryFilter).Process" |  | ||||||
|  |  | ||||||
| 	select { | 	select { | ||||||
| 	case <-ctx.Done(): | 	case <-ctx.Done(): | ||||||
| 		return nil, ctx.Err() | 		return nil, ctx.Err() | ||||||
| @@ -73,12 +68,12 @@ func (f *EntryFilter) Process(ctx context.Context, e *eventlogger.Event) (*event | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if e == nil { | 	if e == nil { | ||||||
| 		return nil, fmt.Errorf("%s: event is nil: %w", op, event.ErrInvalidParameter) | 		return nil, fmt.Errorf("event is nil: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	a, ok := e.Payload.(*AuditEvent) | 	a, ok := e.Payload.(*AuditEvent) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return nil, fmt.Errorf("%s: cannot parse event payload: %w", op, event.ErrInvalidParameter) | 		return nil, fmt.Errorf("cannot parse event payload: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// If we don't have data to process, then we're done. | 	// If we don't have data to process, then we're done. | ||||||
| @@ -88,14 +83,14 @@ func (f *EntryFilter) Process(ctx context.Context, e *eventlogger.Event) (*event | |||||||
|  |  | ||||||
| 	ns, err := namespace.FromContext(ctx) | 	ns, err := namespace.FromContext(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: cannot obtain namespace: %w", op, err) | 		return nil, fmt.Errorf("cannot obtain namespace: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	datum := a.Data.BexprDatum(ns.Path) | 	datum := a.Data.BexprDatum(ns.Path) | ||||||
|  |  | ||||||
| 	result, err := f.evaluator.Evaluate(datum) | 	result, err := f.evaluator.Evaluate(datum) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: unable to evaluate filter: %w", op, err) | 		return nil, fmt.Errorf("unable to evaluate filter: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if result { | 	if result { | ||||||
|   | |||||||
| @@ -27,22 +27,22 @@ func TestEntryFilter_NewEntryFilter(t *testing.T) { | |||||||
| 		"empty-filter": { | 		"empty-filter": { | ||||||
| 			Filter:               "", | 			Filter:               "", | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.NewEntryFilter: cannot create new audit filter with empty filter expression: invalid parameter", | 			ExpectedErrorMessage: "cannot create new audit filter with empty filter expression: invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"spacey-filter": { | 		"spacey-filter": { | ||||||
| 			Filter:               "    ", | 			Filter:               "    ", | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.NewEntryFilter: cannot create new audit filter with empty filter expression: invalid parameter", | 			ExpectedErrorMessage: "cannot create new audit filter with empty filter expression: invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"bad-filter": { | 		"bad-filter": { | ||||||
| 			Filter:               "____", | 			Filter:               "____", | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.NewEntryFilter: cannot create new audit filter", | 			ExpectedErrorMessage: "cannot create new audit filter", | ||||||
| 		}, | 		}, | ||||||
| 		"unsupported-field-filter": { | 		"unsupported-field-filter": { | ||||||
| 			Filter:               "foo == bar", | 			Filter:               "foo == bar", | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.NewEntryFilter: filter references an unsupported field: foo == bar", | 			ExpectedErrorMessage: "filter references an unsupported field: foo == bar", | ||||||
| 		}, | 		}, | ||||||
| 		"good-filter-operation": { | 		"good-filter-operation": { | ||||||
| 			Filter:          "operation == create", | 			Filter:          "operation == create", | ||||||
| @@ -122,7 +122,7 @@ func TestEntryFilter_Process_ContextDone(t *testing.T) { | |||||||
|  |  | ||||||
| 	// Fake event logger event | 	// Fake event logger event | ||||||
| 	e := &eventlogger.Event{ | 	e := &eventlogger.Event{ | ||||||
| 		Type:      eventlogger.EventType(event.AuditType.String()), | 		Type:      event.AuditType.AsEventType(), | ||||||
| 		CreatedAt: time.Now(), | 		CreatedAt: time.Now(), | ||||||
| 		Formatted: make(map[string][]byte), | 		Formatted: make(map[string][]byte), | ||||||
| 		Payload:   a, | 		Payload:   a, | ||||||
| @@ -146,7 +146,7 @@ func TestEntryFilter_Process_NilEvent(t *testing.T) { | |||||||
| 	require.NoError(t, err) | 	require.NoError(t, err) | ||||||
| 	e, err := l.Process(context.Background(), nil) | 	e, err := l.Process(context.Background(), nil) | ||||||
| 	require.Error(t, err) | 	require.Error(t, err) | ||||||
| 	require.EqualError(t, err, "audit.(EntryFilter).Process: event is nil: invalid parameter") | 	require.EqualError(t, err, "event is nil: invalid internal parameter") | ||||||
|  |  | ||||||
| 	// Ensure that the pipeline won't continue. | 	// Ensure that the pipeline won't continue. | ||||||
| 	require.Nil(t, e) | 	require.Nil(t, e) | ||||||
| @@ -162,7 +162,7 @@ func TestEntryFilter_Process_BadPayload(t *testing.T) { | |||||||
| 	require.NoError(t, err) | 	require.NoError(t, err) | ||||||
|  |  | ||||||
| 	e := &eventlogger.Event{ | 	e := &eventlogger.Event{ | ||||||
| 		Type:      eventlogger.EventType(event.AuditType.String()), | 		Type:      event.AuditType.AsEventType(), | ||||||
| 		CreatedAt: time.Now(), | 		CreatedAt: time.Now(), | ||||||
| 		Formatted: make(map[string][]byte), | 		Formatted: make(map[string][]byte), | ||||||
| 		Payload:   nil, | 		Payload:   nil, | ||||||
| @@ -170,7 +170,7 @@ func TestEntryFilter_Process_BadPayload(t *testing.T) { | |||||||
|  |  | ||||||
| 	e2, err := l.Process(context.Background(), e) | 	e2, err := l.Process(context.Background(), e) | ||||||
| 	require.Error(t, err) | 	require.Error(t, err) | ||||||
| 	require.EqualError(t, err, "audit.(EntryFilter).Process: cannot parse event payload: invalid parameter") | 	require.EqualError(t, err, "cannot parse event payload: invalid internal parameter") | ||||||
|  |  | ||||||
| 	// Ensure that the pipeline won't continue. | 	// Ensure that the pipeline won't continue. | ||||||
| 	require.Nil(t, e2) | 	require.Nil(t, e2) | ||||||
| @@ -191,7 +191,7 @@ func TestEntryFilter_Process_NoAuditDataInPayload(t *testing.T) { | |||||||
| 	a.Data = nil | 	a.Data = nil | ||||||
|  |  | ||||||
| 	e := &eventlogger.Event{ | 	e := &eventlogger.Event{ | ||||||
| 		Type:      eventlogger.EventType(event.AuditType.String()), | 		Type:      event.AuditType.AsEventType(), | ||||||
| 		CreatedAt: time.Now(), | 		CreatedAt: time.Now(), | ||||||
| 		Formatted: make(map[string][]byte), | 		Formatted: make(map[string][]byte), | ||||||
| 		Payload:   a, | 		Payload:   a, | ||||||
| @@ -223,7 +223,7 @@ func TestEntryFilter_Process_FilterSuccess(t *testing.T) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	e := &eventlogger.Event{ | 	e := &eventlogger.Event{ | ||||||
| 		Type:      eventlogger.EventType(event.AuditType.String()), | 		Type:      event.AuditType.AsEventType(), | ||||||
| 		CreatedAt: time.Now(), | 		CreatedAt: time.Now(), | ||||||
| 		Formatted: make(map[string][]byte), | 		Formatted: make(map[string][]byte), | ||||||
| 		Payload:   a, | 		Payload:   a, | ||||||
| @@ -256,7 +256,7 @@ func TestEntryFilter_Process_FilterFail(t *testing.T) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	e := &eventlogger.Event{ | 	e := &eventlogger.Event{ | ||||||
| 		Type:      eventlogger.EventType(event.AuditType.String()), | 		Type:      event.AuditType.AsEventType(), | ||||||
| 		CreatedAt: time.Now(), | 		CreatedAt: time.Now(), | ||||||
| 		Formatted: make(map[string][]byte), | 		Formatted: make(map[string][]byte), | ||||||
| 		Payload:   a, | 		Payload:   a, | ||||||
|   | |||||||
| @@ -18,7 +18,6 @@ import ( | |||||||
| 	"github.com/hashicorp/go-hclog" | 	"github.com/hashicorp/go-hclog" | ||||||
| 	"github.com/hashicorp/go-multierror" | 	"github.com/hashicorp/go-multierror" | ||||||
| 	"github.com/hashicorp/vault/helper/namespace" | 	"github.com/hashicorp/vault/helper/namespace" | ||||||
| 	"github.com/hashicorp/vault/internal/observability/event" |  | ||||||
| 	"github.com/hashicorp/vault/sdk/helper/jsonutil" | 	"github.com/hashicorp/vault/sdk/helper/jsonutil" | ||||||
| 	"github.com/hashicorp/vault/sdk/helper/salt" | 	"github.com/hashicorp/vault/sdk/helper/salt" | ||||||
| 	"github.com/hashicorp/vault/sdk/logical" | 	"github.com/hashicorp/vault/sdk/logical" | ||||||
| @@ -36,7 +35,49 @@ type timeProvider interface { | |||||||
| 	formattedTime() string | 	formattedTime() string | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // FormatterConfig is used to provide basic configuration to a formatter. | ||||||
|  | // Use NewFormatterConfig to initialize the FormatterConfig struct. | ||||||
|  | type FormatterConfig struct { | ||||||
|  | 	Raw          bool | ||||||
|  | 	HMACAccessor bool | ||||||
|  |  | ||||||
|  | 	// Vault lacks pagination in its APIs. As a result, certain list operations can return **very** large responses. | ||||||
|  | 	// The user's chosen audit sinks may experience difficulty consuming audit records that swell to tens of megabytes | ||||||
|  | 	// of JSON. The responses of list operations are typically not very interesting, as they are mostly lists of keys, | ||||||
|  | 	// or, even when they include a "key_info" field, are not returning confidential information. They become even less | ||||||
|  | 	// interesting once HMAC-ed by the audit system. | ||||||
|  | 	// | ||||||
|  | 	// Some example Vault "list" operations that are prone to becoming very large in an active Vault installation are: | ||||||
|  | 	//   auth/token/accessors/ | ||||||
|  | 	//   identity/entity/id/ | ||||||
|  | 	//   identity/entity-alias/id/ | ||||||
|  | 	//   pki/certs/ | ||||||
|  | 	// | ||||||
|  | 	// This option exists to provide such users with the option to have response data elided from audit logs, only when | ||||||
|  | 	// the operation type is "list". For added safety, the elision only applies to the "keys" and "key_info" fields | ||||||
|  | 	// within the response data - these are conventionally the only fields present in a list response - see | ||||||
|  | 	// logical.ListResponse, and logical.ListResponseWithInfo. However, other fields are technically possible if a | ||||||
|  | 	// plugin author writes unusual code, and these will be preserved in the audit log even with this option enabled. | ||||||
|  | 	// The elision replaces the values of the "keys" and "key_info" fields with an integer count of the number of | ||||||
|  | 	// entries. This allows even the elided audit logs to still be useful for answering questions like | ||||||
|  | 	// "Was any data returned?" or "How many records were listed?". | ||||||
|  | 	ElideListResponses bool | ||||||
|  |  | ||||||
|  | 	// This should only ever be used in a testing context | ||||||
|  | 	OmitTime bool | ||||||
|  |  | ||||||
|  | 	// The required/target format for the event (supported: JSONFormat and JSONxFormat). | ||||||
|  | 	RequiredFormat format | ||||||
|  |  | ||||||
|  | 	// headerFormatter specifies the formatter used for headers that existing in any incoming audit request. | ||||||
|  | 	headerFormatter HeaderFormatter | ||||||
|  |  | ||||||
|  | 	// Prefix specifies a Prefix that should be prepended to any formatted request or response before serialization. | ||||||
|  | 	Prefix string | ||||||
|  | } | ||||||
|  |  | ||||||
| // EntryFormatter should be used to format audit requests and responses. | // EntryFormatter should be used to format audit requests and responses. | ||||||
|  | // NOTE: Use NewEntryFormatter to initialize the EntryFormatter struct. | ||||||
| type EntryFormatter struct { | type EntryFormatter struct { | ||||||
| 	config FormatterConfig | 	config FormatterConfig | ||||||
| 	salter Salter | 	salter Salter | ||||||
| @@ -44,26 +85,42 @@ type EntryFormatter struct { | |||||||
| 	name   string | 	name   string | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // NewFormatterConfig should be used to create a FormatterConfig. | ||||||
|  | // Accepted options: WithElision, WithFormat, WithHMACAccessor, WithOmitTime, WithPrefix, WithRaw. | ||||||
|  | func NewFormatterConfig(headerFormatter HeaderFormatter, opt ...Option) (FormatterConfig, error) { | ||||||
|  | 	if headerFormatter == nil || reflect.ValueOf(headerFormatter).IsNil() { | ||||||
|  | 		return FormatterConfig{}, fmt.Errorf("header formatter is required: %w", ErrInvalidParameter) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	opts, err := getOpts(opt...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return FormatterConfig{}, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return FormatterConfig{ | ||||||
|  | 		headerFormatter:    headerFormatter, | ||||||
|  | 		ElideListResponses: opts.withElision, | ||||||
|  | 		HMACAccessor:       opts.withHMACAccessor, | ||||||
|  | 		OmitTime:           opts.withOmitTime, | ||||||
|  | 		Prefix:             opts.withPrefix, | ||||||
|  | 		Raw:                opts.withRaw, | ||||||
|  | 		RequiredFormat:     opts.withFormat, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
| // NewEntryFormatter should be used to create an EntryFormatter. | // NewEntryFormatter should be used to create an EntryFormatter. | ||||||
| func NewEntryFormatter(name string, config FormatterConfig, salter Salter, logger hclog.Logger) (*EntryFormatter, error) { | func NewEntryFormatter(name string, config FormatterConfig, salter Salter, logger hclog.Logger) (*EntryFormatter, error) { | ||||||
| 	const op = "audit.NewEntryFormatter" |  | ||||||
|  |  | ||||||
| 	name = strings.TrimSpace(name) | 	name = strings.TrimSpace(name) | ||||||
| 	if name == "" { | 	if name == "" { | ||||||
| 		return nil, fmt.Errorf("%s: name is required: %w", op, event.ErrInvalidParameter) | 		return nil, fmt.Errorf("name is required: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if salter == nil { | 	if salter == nil { | ||||||
| 		return nil, fmt.Errorf("%s: cannot create a new audit formatter with nil salter: %w", op, event.ErrInvalidParameter) | 		return nil, fmt.Errorf("cannot create a new audit formatter with nil salter: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if logger == nil || reflect.ValueOf(logger).IsNil() { | 	if logger == nil || reflect.ValueOf(logger).IsNil() { | ||||||
| 		return nil, fmt.Errorf("%s: cannot create a new audit formatter with nil logger: %w", op, event.ErrInvalidParameter) | 		return nil, fmt.Errorf("cannot create a new audit formatter with nil logger: %w", ErrInvalidParameter) | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// We need to ensure that the format isn't just some default empty string. |  | ||||||
| 	if err := config.RequiredFormat.validate(); err != nil { |  | ||||||
| 		return nil, fmt.Errorf("%s: format not valid: %w", op, err) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return &EntryFormatter{ | 	return &EntryFormatter{ | ||||||
| @@ -87,8 +144,6 @@ func (*EntryFormatter) Type() eventlogger.NodeType { | |||||||
| // Process will attempt to parse the incoming event data into a corresponding | // Process will attempt to parse the incoming event data into a corresponding | ||||||
| // audit Request/Response which is serialized to JSON/JSONx and stored within the event. | // audit Request/Response which is serialized to JSON/JSONx and stored within the event. | ||||||
| func (f *EntryFormatter) Process(ctx context.Context, e *eventlogger.Event) (_ *eventlogger.Event, retErr error) { | func (f *EntryFormatter) Process(ctx context.Context, e *eventlogger.Event) (_ *eventlogger.Event, retErr error) { | ||||||
| 	const op = "audit.(EntryFormatter).Process" |  | ||||||
|  |  | ||||||
| 	// Return early if the context was cancelled, eventlogger will not carry on | 	// Return early if the context was cancelled, eventlogger will not carry on | ||||||
| 	// asking nodes to process, so any sink node in the pipeline won't be called. | 	// asking nodes to process, so any sink node in the pipeline won't be called. | ||||||
| 	select { | 	select { | ||||||
| @@ -100,16 +155,16 @@ func (f *EntryFormatter) Process(ctx context.Context, e *eventlogger.Event) (_ * | |||||||
| 	// Perform validation on the event, then retrieve the underlying AuditEvent | 	// Perform validation on the event, then retrieve the underlying AuditEvent | ||||||
| 	// and LogInput (from the AuditEvent Data). | 	// and LogInput (from the AuditEvent Data). | ||||||
| 	if e == nil { | 	if e == nil { | ||||||
| 		return nil, fmt.Errorf("%s: event is nil: %w", op, event.ErrInvalidParameter) | 		return nil, fmt.Errorf("event is nil: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	a, ok := e.Payload.(*AuditEvent) | 	a, ok := e.Payload.(*AuditEvent) | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return nil, fmt.Errorf("%s: cannot parse event payload: %w", op, event.ErrInvalidParameter) | 		return nil, fmt.Errorf("cannot parse event payload: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if a.Data == nil { | 	if a.Data == nil { | ||||||
| 		return nil, fmt.Errorf("%s: cannot audit event (%s) with no data: %w", op, a.Subtype, event.ErrInvalidParameter) | 		return nil, fmt.Errorf("cannot audit event (%s) with no data: %w", a.Subtype, ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Handle panics | 	// Handle panics | ||||||
| @@ -126,13 +181,13 @@ func (f *EntryFormatter) Process(ctx context.Context, e *eventlogger.Event) (_ * | |||||||
| 			"stacktrace", string(debug.Stack())) | 			"stacktrace", string(debug.Stack())) | ||||||
|  |  | ||||||
| 		// Ensure that we add this error onto any pre-existing error that was being returned. | 		// Ensure that we add this error onto any pre-existing error that was being returned. | ||||||
| 		retErr = multierror.Append(retErr, fmt.Errorf("%s: panic generating audit log: %q", op, f.name)).ErrorOrNil() | 		retErr = multierror.Append(retErr, fmt.Errorf("panic generating audit log: %q", f.name)).ErrorOrNil() | ||||||
| 	}() | 	}() | ||||||
|  |  | ||||||
| 	// Take a copy of the event data before we modify anything. | 	// Take a copy of the event data before we modify anything. | ||||||
| 	data, err := a.Data.Clone() | 	data, err := a.Data.Clone() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: unable to copy audit event data: %w", op, err) | 		return nil, fmt.Errorf("unable to clone audit event data: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// If the request is present in the input data, apply header configuration | 	// If the request is present in the input data, apply header configuration | ||||||
| @@ -144,7 +199,7 @@ func (f *EntryFormatter) Process(ctx context.Context, e *eventlogger.Event) (_ * | |||||||
| 		// e.g. via: /sys/config/auditing/request-headers/:name | 		// e.g. via: /sys/config/auditing/request-headers/:name | ||||||
| 		data.Request.Headers, err = f.config.headerFormatter.ApplyConfig(ctx, data.Request.Headers, f.salter) | 		data.Request.Headers, err = f.config.headerFormatter.ApplyConfig(ctx, data.Request.Headers, f.salter) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("%s: unable to transform headers for auditing: %w", op, err) | 			return nil, fmt.Errorf("unable to transform headers for auditing: %w", err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -165,25 +220,25 @@ func (f *EntryFormatter) Process(ctx context.Context, e *eventlogger.Event) (_ * | |||||||
| 	case ResponseType: | 	case ResponseType: | ||||||
| 		entry, err = f.FormatResponse(ctx, data, a) | 		entry, err = f.FormatResponse(ctx, data, a) | ||||||
| 	default: | 	default: | ||||||
| 		return nil, fmt.Errorf("%s: unknown audit event subtype: %q", op, a.Subtype) | 		return nil, fmt.Errorf("unknown audit event subtype: %q", a.Subtype) | ||||||
| 	} | 	} | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: unable to parse %s from audit event: %w", op, a.Subtype.String(), err) | 		return nil, fmt.Errorf("unable to parse %s from audit event: %w", a.Subtype, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	result, err := jsonutil.EncodeJSON(entry) | 	result, err := jsonutil.EncodeJSON(entry) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: unable to format %s: %w", op, a.Subtype.String(), err) | 		return nil, fmt.Errorf("unable to format %s: %w", a.Subtype, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if f.config.RequiredFormat == JSONxFormat { | 	if f.config.RequiredFormat == JSONxFormat { | ||||||
| 		var err error | 		var err error | ||||||
| 		result, err = jsonx.EncodeJSONBytes(result) | 		result, err = jsonx.EncodeJSONBytes(result) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("%s: unable to encode JSONx using JSON data: %w", op, err) | 			return nil, fmt.Errorf("unable to encode JSONx using JSON data: %w", err) | ||||||
| 		} | 		} | ||||||
| 		if result == nil { | 		if result == nil { | ||||||
| 			return nil, fmt.Errorf("%s: encoded JSONx was nil: %w", op, err) | 			return nil, fmt.Errorf("encoded JSONx was nil: %w", err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -569,31 +624,6 @@ func (f *EntryFormatter) FormatResponse(ctx context.Context, in *logical.LogInpu | |||||||
| 	return respEntry, nil | 	return respEntry, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // NewFormatterConfig should be used to create a FormatterConfig. |  | ||||||
| // Accepted options: WithElision, WithFormat, WithHMACAccessor, WithOmitTime, WithPrefix, WithRaw. |  | ||||||
| func NewFormatterConfig(headerFormatter HeaderFormatter, opt ...Option) (FormatterConfig, error) { |  | ||||||
| 	const op = "audit.NewFormatterConfig" |  | ||||||
|  |  | ||||||
| 	if headerFormatter == nil || reflect.ValueOf(headerFormatter).IsNil() { |  | ||||||
| 		return FormatterConfig{}, fmt.Errorf("%s: header formatter is required: %w", op, event.ErrInvalidParameter) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	opts, err := getOpts(opt...) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return FormatterConfig{}, fmt.Errorf("%s: error applying options: %w", op, err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return FormatterConfig{ |  | ||||||
| 		headerFormatter:    headerFormatter, |  | ||||||
| 		ElideListResponses: opts.withElision, |  | ||||||
| 		HMACAccessor:       opts.withHMACAccessor, |  | ||||||
| 		OmitTime:           opts.withOmitTime, |  | ||||||
| 		Prefix:             opts.withPrefix, |  | ||||||
| 		Raw:                opts.withRaw, |  | ||||||
| 		RequiredFormat:     opts.withFormat, |  | ||||||
| 	}, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // getRemoteAddr safely gets the remote address avoiding a nil pointer | // getRemoteAddr safely gets the remote address avoiding a nil pointer | ||||||
| func getRemoteAddr(req *logical.Request) string { | func getRemoteAddr(req *logical.Request) string { | ||||||
| 	if req != nil && req.Connection != nil { | 	if req != nil && req.Connection != nil { | ||||||
|   | |||||||
| @@ -103,25 +103,25 @@ func TestNewEntryFormatter(t *testing.T) { | |||||||
| 		"empty-name": { | 		"empty-name": { | ||||||
| 			Name:                 "", | 			Name:                 "", | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.NewEntryFormatter: name is required: invalid parameter", | 			ExpectedErrorMessage: "name is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"spacey-name": { | 		"spacey-name": { | ||||||
| 			Name:                 "   ", | 			Name:                 "   ", | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.NewEntryFormatter: name is required: invalid parameter", | 			ExpectedErrorMessage: "name is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"nil-salter": { | 		"nil-salter": { | ||||||
| 			Name:                 "juan", | 			Name:                 "juan", | ||||||
| 			UseStaticSalt:        false, | 			UseStaticSalt:        false, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.NewEntryFormatter: cannot create a new audit formatter with nil salter: invalid parameter", | 			ExpectedErrorMessage: "cannot create a new audit formatter with nil salter: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"nil-logger": { | 		"nil-logger": { | ||||||
| 			Name:                 "juan", | 			Name:                 "juan", | ||||||
| 			UseStaticSalt:        true, | 			UseStaticSalt:        true, | ||||||
| 			Logger:               nil, | 			Logger:               nil, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.NewEntryFormatter: cannot create a new audit formatter with nil logger: invalid parameter", | 			ExpectedErrorMessage: "cannot create a new audit formatter with nil logger: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"static-salter": { | 		"static-salter": { | ||||||
| 			Name:            "juan", | 			Name:            "juan", | ||||||
| @@ -258,42 +258,42 @@ func TestEntryFormatter_Process(t *testing.T) { | |||||||
| 	}{ | 	}{ | ||||||
| 		"json-request-no-data": { | 		"json-request-no-data": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(EntryFormatter).Process: cannot audit event (request) with no data: invalid parameter", | 			ExpectedErrorMessage: "cannot audit event (request) with no data: invalid internal parameter", | ||||||
| 			Subtype:              RequestType, | 			Subtype:              RequestType, | ||||||
| 			RequiredFormat:       JSONFormat, | 			RequiredFormat:       JSONFormat, | ||||||
| 			Data:                 nil, | 			Data:                 nil, | ||||||
| 		}, | 		}, | ||||||
| 		"json-response-no-data": { | 		"json-response-no-data": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(EntryFormatter).Process: cannot audit event (response) with no data: invalid parameter", | 			ExpectedErrorMessage: "cannot audit event (response) with no data: invalid internal parameter", | ||||||
| 			Subtype:              ResponseType, | 			Subtype:              ResponseType, | ||||||
| 			RequiredFormat:       JSONFormat, | 			RequiredFormat:       JSONFormat, | ||||||
| 			Data:                 nil, | 			Data:                 nil, | ||||||
| 		}, | 		}, | ||||||
| 		"json-request-basic-input": { | 		"json-request-basic-input": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse request from audit event: request to request-audit a nil request", | 			ExpectedErrorMessage: "unable to parse request from audit event: request to request-audit a nil request", | ||||||
| 			Subtype:              RequestType, | 			Subtype:              RequestType, | ||||||
| 			RequiredFormat:       JSONFormat, | 			RequiredFormat:       JSONFormat, | ||||||
| 			Data:                 &logical.LogInput{Type: "magic"}, | 			Data:                 &logical.LogInput{Type: "magic"}, | ||||||
| 		}, | 		}, | ||||||
| 		"json-response-basic-input": { | 		"json-response-basic-input": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse response from audit event: request to response-audit a nil request", | 			ExpectedErrorMessage: "unable to parse response from audit event: request to response-audit a nil request", | ||||||
| 			Subtype:              ResponseType, | 			Subtype:              ResponseType, | ||||||
| 			RequiredFormat:       JSONFormat, | 			RequiredFormat:       JSONFormat, | ||||||
| 			Data:                 &logical.LogInput{Type: "magic"}, | 			Data:                 &logical.LogInput{Type: "magic"}, | ||||||
| 		}, | 		}, | ||||||
| 		"json-request-basic-input-and-request-no-ns": { | 		"json-request-basic-input-and-request-no-ns": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse request from audit event: no namespace", | 			ExpectedErrorMessage: "unable to parse request from audit event: no namespace", | ||||||
| 			Subtype:              RequestType, | 			Subtype:              RequestType, | ||||||
| 			RequiredFormat:       JSONFormat, | 			RequiredFormat:       JSONFormat, | ||||||
| 			Data:                 &logical.LogInput{Request: &logical.Request{ID: "123"}}, | 			Data:                 &logical.LogInput{Request: &logical.Request{ID: "123"}}, | ||||||
| 		}, | 		}, | ||||||
| 		"json-response-basic-input-and-request-no-ns": { | 		"json-response-basic-input-and-request-no-ns": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse response from audit event: no namespace", | 			ExpectedErrorMessage: "unable to parse response from audit event: no namespace", | ||||||
| 			Subtype:              ResponseType, | 			Subtype:              ResponseType, | ||||||
| 			RequiredFormat:       JSONFormat, | 			RequiredFormat:       JSONFormat, | ||||||
| 			Data:                 &logical.LogInput{Request: &logical.Request{ID: "123"}}, | 			Data:                 &logical.LogInput{Request: &logical.Request{ID: "123"}}, | ||||||
| @@ -314,42 +314,42 @@ func TestEntryFormatter_Process(t *testing.T) { | |||||||
| 		}, | 		}, | ||||||
| 		"jsonx-request-no-data": { | 		"jsonx-request-no-data": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(EntryFormatter).Process: cannot audit event (request) with no data: invalid parameter", | 			ExpectedErrorMessage: "cannot audit event (request) with no data: invalid internal parameter", | ||||||
| 			Subtype:              RequestType, | 			Subtype:              RequestType, | ||||||
| 			RequiredFormat:       JSONxFormat, | 			RequiredFormat:       JSONxFormat, | ||||||
| 			Data:                 nil, | 			Data:                 nil, | ||||||
| 		}, | 		}, | ||||||
| 		"jsonx-response-no-data": { | 		"jsonx-response-no-data": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(EntryFormatter).Process: cannot audit event (response) with no data: invalid parameter", | 			ExpectedErrorMessage: "cannot audit event (response) with no data: invalid internal parameter", | ||||||
| 			Subtype:              ResponseType, | 			Subtype:              ResponseType, | ||||||
| 			RequiredFormat:       JSONxFormat, | 			RequiredFormat:       JSONxFormat, | ||||||
| 			Data:                 nil, | 			Data:                 nil, | ||||||
| 		}, | 		}, | ||||||
| 		"jsonx-request-basic-input": { | 		"jsonx-request-basic-input": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse request from audit event: request to request-audit a nil request", | 			ExpectedErrorMessage: "unable to parse request from audit event: request to request-audit a nil request", | ||||||
| 			Subtype:              RequestType, | 			Subtype:              RequestType, | ||||||
| 			RequiredFormat:       JSONxFormat, | 			RequiredFormat:       JSONxFormat, | ||||||
| 			Data:                 &logical.LogInput{Type: "magic"}, | 			Data:                 &logical.LogInput{Type: "magic"}, | ||||||
| 		}, | 		}, | ||||||
| 		"jsonx-response-basic-input": { | 		"jsonx-response-basic-input": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse response from audit event: request to response-audit a nil request", | 			ExpectedErrorMessage: "unable to parse response from audit event: request to response-audit a nil request", | ||||||
| 			Subtype:              ResponseType, | 			Subtype:              ResponseType, | ||||||
| 			RequiredFormat:       JSONxFormat, | 			RequiredFormat:       JSONxFormat, | ||||||
| 			Data:                 &logical.LogInput{Type: "magic"}, | 			Data:                 &logical.LogInput{Type: "magic"}, | ||||||
| 		}, | 		}, | ||||||
| 		"jsonx-request-basic-input-and-request-no-ns": { | 		"jsonx-request-basic-input-and-request-no-ns": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse request from audit event: no namespace", | 			ExpectedErrorMessage: "unable to parse request from audit event: no namespace", | ||||||
| 			Subtype:              RequestType, | 			Subtype:              RequestType, | ||||||
| 			RequiredFormat:       JSONxFormat, | 			RequiredFormat:       JSONxFormat, | ||||||
| 			Data:                 &logical.LogInput{Request: &logical.Request{ID: "123"}}, | 			Data:                 &logical.LogInput{Request: &logical.Request{ID: "123"}}, | ||||||
| 		}, | 		}, | ||||||
| 		"jsonx-response-basic-input-and-request-no-ns": { | 		"jsonx-response-basic-input-and-request-no-ns": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse response from audit event: no namespace", | 			ExpectedErrorMessage: "unable to parse response from audit event: no namespace", | ||||||
| 			Subtype:              ResponseType, | 			Subtype:              ResponseType, | ||||||
| 			RequiredFormat:       JSONxFormat, | 			RequiredFormat:       JSONxFormat, | ||||||
| 			Data:                 &logical.LogInput{Request: &logical.Request{ID: "123"}}, | 			Data:                 &logical.LogInput{Request: &logical.Request{ID: "123"}}, | ||||||
| @@ -738,7 +738,7 @@ func TestEntryFormatter_Process_JSON(t *testing.T) { | |||||||
| 		auditEvent.Data = in | 		auditEvent.Data = in | ||||||
|  |  | ||||||
| 		e := &eventlogger.Event{ | 		e := &eventlogger.Event{ | ||||||
| 			Type:      eventlogger.EventType(event.AuditType.String()), | 			Type:      event.AuditType.AsEventType(), | ||||||
| 			CreatedAt: time.Now(), | 			CreatedAt: time.Now(), | ||||||
| 			Formatted: make(map[string][]byte), | 			Formatted: make(map[string][]byte), | ||||||
| 			Payload:   auditEvent, | 			Payload:   auditEvent, | ||||||
| @@ -902,7 +902,7 @@ func TestEntryFormatter_Process_JSONx(t *testing.T) { | |||||||
| 		auditEvent.Data = in | 		auditEvent.Data = in | ||||||
|  |  | ||||||
| 		e := &eventlogger.Event{ | 		e := &eventlogger.Event{ | ||||||
| 			Type:      eventlogger.EventType(event.AuditType.String()), | 			Type:      event.AuditType.AsEventType(), | ||||||
| 			CreatedAt: time.Now(), | 			CreatedAt: time.Now(), | ||||||
| 			Formatted: make(map[string][]byte), | 			Formatted: make(map[string][]byte), | ||||||
| 			Payload:   auditEvent, | 			Payload:   auditEvent, | ||||||
| @@ -1169,7 +1169,7 @@ func TestEntryFormatter_Process_Panic(t *testing.T) { | |||||||
|  |  | ||||||
| 	e2, err := formatter.Process(namespace.RootContext(nil), e) | 	e2, err := formatter.Process(namespace.RootContext(nil), e) | ||||||
| 	require.Error(t, err) | 	require.Error(t, err) | ||||||
| 	require.Contains(t, err.Error(), "audit.(EntryFormatter).Process: panic generating audit log: \"juan\"") | 	require.Contains(t, err.Error(), "panic generating audit log: \"juan\"") | ||||||
| 	require.Nil(t, e2) | 	require.Nil(t, e2) | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
							
								
								
									
										34
									
								
								audit/errors.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								audit/errors.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | |||||||
|  | // Copyright (c) HashiCorp, Inc. | ||||||
|  | // SPDX-License-Identifier: BUSL-1.1 | ||||||
|  |  | ||||||
|  | package audit | ||||||
|  |  | ||||||
|  | import "errors" | ||||||
|  |  | ||||||
|  | var ( | ||||||
|  | 	// ErrInternal should be used to represent an unexpected error that occurred | ||||||
|  | 	// within the audit system. | ||||||
|  | 	ErrInternal = errors.New("audit system internal error") | ||||||
|  |  | ||||||
|  | 	// ErrInvalidParameter should be used to represent an error in which the | ||||||
|  | 	// internal audit system is receiving invalid parameters from other parts of | ||||||
|  | 	// Vault which should have already been validated. | ||||||
|  | 	ErrInvalidParameter = errors.New("invalid internal parameter") | ||||||
|  |  | ||||||
|  | 	// ErrExternalOptions should be used to represent an error related to | ||||||
|  | 	// invalid configuration provided to Vault (i.e. by the Vault Operator). | ||||||
|  | 	ErrExternalOptions = errors.New("invalid configuration") | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // ConvertToExternalError handles converting an error that was generated in Vault | ||||||
|  | // and should appear as-is in the server logs, to an error that can be returned to | ||||||
|  | // calling clients (via the API/CLI). | ||||||
|  | func ConvertToExternalError(err error) error { | ||||||
|  | 	// If the error is an internal error, the contents will have been logged, and | ||||||
|  | 	// we should probably shield the caller from the details. | ||||||
|  | 	if errors.Is(err, ErrInternal) { | ||||||
|  | 		return ErrInternal | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return err | ||||||
|  | } | ||||||
							
								
								
									
										26
									
								
								audit/errors_test.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								audit/errors_test.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,26 @@ | |||||||
|  | // Copyright (c) HashiCorp, Inc. | ||||||
|  | // SPDX-License-Identifier: BUSL-1.1 | ||||||
|  |  | ||||||
|  | package audit | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 	"fmt" | ||||||
|  | 	"testing" | ||||||
|  |  | ||||||
|  | 	"github.com/stretchr/testify/require" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // TestErrors_ConvertToExternalError is used to check that we 'mute' errors which | ||||||
|  | // have an internal error in their tree. | ||||||
|  | func TestErrors_ConvertToExternalError(t *testing.T) { | ||||||
|  | 	t.Parallel() | ||||||
|  |  | ||||||
|  | 	err := fmt.Errorf("wrap this error: %w", ErrInternal) | ||||||
|  | 	res := ConvertToExternalError(err) | ||||||
|  | 	require.EqualError(t, res, "audit system internal error") | ||||||
|  |  | ||||||
|  | 	err = fmt.Errorf("test: %w", errors.New("this is just an error")) | ||||||
|  | 	res = ConvertToExternalError(err) | ||||||
|  | 	require.Equal(t, "test: this is just an error", res.Error()) | ||||||
|  | } | ||||||
| @@ -5,6 +5,7 @@ package audit | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"strings" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/hashicorp/vault/internal/observability/event" | 	"github.com/hashicorp/vault/internal/observability/event" | ||||||
| @@ -48,12 +49,10 @@ type subtype string | |||||||
| // for audit events. It will generate an ID if no ID is supplied. Supported | // for audit events. It will generate an ID if no ID is supplied. Supported | ||||||
| // options: WithID, WithNow. | // options: WithID, WithNow. | ||||||
| func NewEvent(s subtype, opt ...Option) (*AuditEvent, error) { | func NewEvent(s subtype, opt ...Option) (*AuditEvent, error) { | ||||||
| 	const op = "audit.NewEvent" |  | ||||||
|  |  | ||||||
| 	// Get the default options | 	// Get the default options | ||||||
| 	opts, err := getOpts(opt...) | 	opts, err := getOpts(opt...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error applying options: %w", op, err) | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if opts.withID == "" { | 	if opts.withID == "" { | ||||||
| @@ -61,7 +60,7 @@ func NewEvent(s subtype, opt ...Option) (*AuditEvent, error) { | |||||||
|  |  | ||||||
| 		opts.withID, err = event.NewID(string(event.AuditType)) | 		opts.withID, err = event.NewID(string(event.AuditType)) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("%s: error creating ID for event: %w", op, err) | 			return nil, fmt.Errorf("error creating ID for event: %w", err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -73,34 +72,32 @@ func NewEvent(s subtype, opt ...Option) (*AuditEvent, error) { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := audit.validate(); err != nil { | 	if err := audit.validate(); err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: %w", op, err) | 		return nil, err | ||||||
| 	} | 	} | ||||||
| 	return audit, nil | 	return audit, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // validate attempts to ensure the audit event in its present state is valid. | // validate attempts to ensure the audit event in its present state is valid. | ||||||
| func (a *AuditEvent) validate() error { | func (a *AuditEvent) validate() error { | ||||||
| 	const op = "audit.(AuditEvent).validate" |  | ||||||
|  |  | ||||||
| 	if a == nil { | 	if a == nil { | ||||||
| 		return fmt.Errorf("%s: event is nil: %w", op, event.ErrInvalidParameter) | 		return fmt.Errorf("event is nil: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if a.ID == "" { | 	if a.ID == "" { | ||||||
| 		return fmt.Errorf("%s: missing ID: %w", op, event.ErrInvalidParameter) | 		return fmt.Errorf("missing ID: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if a.Version != version { | 	if a.Version != version { | ||||||
| 		return fmt.Errorf("%s: event version unsupported: %w", op, event.ErrInvalidParameter) | 		return fmt.Errorf("event version unsupported: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if a.Timestamp.IsZero() { | 	if a.Timestamp.IsZero() { | ||||||
| 		return fmt.Errorf("%s: event timestamp cannot be the zero time instant: %w", op, event.ErrInvalidParameter) | 		return fmt.Errorf("event timestamp cannot be the zero time instant: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err := a.Subtype.validate() | 	err := a.Subtype.validate() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: %w", op, err) | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| @@ -108,23 +105,21 @@ func (a *AuditEvent) validate() error { | |||||||
|  |  | ||||||
| // validate ensures that subtype is one of the set of allowed event subtypes. | // validate ensures that subtype is one of the set of allowed event subtypes. | ||||||
| func (t subtype) validate() error { | func (t subtype) validate() error { | ||||||
| 	const op = "audit.(subtype).validate" |  | ||||||
| 	switch t { | 	switch t { | ||||||
| 	case RequestType, ResponseType: | 	case RequestType, ResponseType: | ||||||
| 		return nil | 		return nil | ||||||
| 	default: | 	default: | ||||||
| 		return fmt.Errorf("%s: '%s' is not a valid event subtype: %w", op, t, event.ErrInvalidParameter) | 		return fmt.Errorf("invalid event subtype %q: %w", t, ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // validate ensures that format is one of the set of allowed event formats. | // validate ensures that format is one of the set of allowed event formats. | ||||||
| func (f format) validate() error { | func (f format) validate() error { | ||||||
| 	const op = "audit.(format).validate" |  | ||||||
| 	switch f { | 	switch f { | ||||||
| 	case JSONFormat, JSONxFormat: | 	case JSONFormat, JSONxFormat: | ||||||
| 		return nil | 		return nil | ||||||
| 	default: | 	default: | ||||||
| 		return fmt.Errorf("%s: '%s' is not a valid format: %w", op, f, event.ErrInvalidParameter) | 		return fmt.Errorf("invalid format %q: %w", f, ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -163,3 +158,10 @@ func (t subtype) String() string { | |||||||
| func (a *AuditEvent) formattedTime() string { | func (a *AuditEvent) formattedTime() string { | ||||||
| 	return a.Timestamp.UTC().Format(time.RFC3339Nano) | 	return a.Timestamp.UTC().Format(time.RFC3339Nano) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // IsValidFormat provides a means to validate whether the supplied format is valid. | ||||||
|  | // Examples of valid formats are JSON and JSONx. | ||||||
|  | func IsValidFormat(v string) bool { | ||||||
|  | 	err := format(strings.TrimSpace(strings.ToLower(v))).validate() | ||||||
|  | 	return err == nil | ||||||
|  | } | ||||||
|   | |||||||
| @@ -31,21 +31,21 @@ func TestAuditEvent_new(t *testing.T) { | |||||||
| 			Subtype:              subtype(""), | 			Subtype:              subtype(""), | ||||||
| 			Format:               format(""), | 			Format:               format(""), | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.NewEvent: audit.(AuditEvent).validate: audit.(subtype).validate: '' is not a valid event subtype: invalid parameter", | 			ExpectedErrorMessage: "invalid event subtype \"\": invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"empty-Option": { | 		"empty-Option": { | ||||||
| 			Options:              []Option{}, | 			Options:              []Option{}, | ||||||
| 			Subtype:              subtype(""), | 			Subtype:              subtype(""), | ||||||
| 			Format:               format(""), | 			Format:               format(""), | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.NewEvent: audit.(AuditEvent).validate: audit.(subtype).validate: '' is not a valid event subtype: invalid parameter", | 			ExpectedErrorMessage: "invalid event subtype \"\": invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"bad-id": { | 		"bad-id": { | ||||||
| 			Options:              []Option{WithID("")}, | 			Options:              []Option{WithID("")}, | ||||||
| 			Subtype:              ResponseType, | 			Subtype:              ResponseType, | ||||||
| 			Format:               JSONFormat, | 			Format:               JSONFormat, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.NewEvent: error applying options: id cannot be empty", | 			ExpectedErrorMessage: "id cannot be empty", | ||||||
| 		}, | 		}, | ||||||
| 		"good": { | 		"good": { | ||||||
| 			Options: []Option{ | 			Options: []Option{ | ||||||
| @@ -119,12 +119,12 @@ func TestAuditEvent_Validate(t *testing.T) { | |||||||
| 		"nil": { | 		"nil": { | ||||||
| 			Value:                nil, | 			Value:                nil, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(AuditEvent).validate: event is nil: invalid parameter", | 			ExpectedErrorMessage: "event is nil: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"default": { | 		"default": { | ||||||
| 			Value:                &AuditEvent{}, | 			Value:                &AuditEvent{}, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(AuditEvent).validate: missing ID: invalid parameter", | 			ExpectedErrorMessage: "missing ID: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"id-empty": { | 		"id-empty": { | ||||||
| 			Value: &AuditEvent{ | 			Value: &AuditEvent{ | ||||||
| @@ -135,7 +135,7 @@ func TestAuditEvent_Validate(t *testing.T) { | |||||||
| 				Data:      nil, | 				Data:      nil, | ||||||
| 			}, | 			}, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(AuditEvent).validate: missing ID: invalid parameter", | 			ExpectedErrorMessage: "missing ID: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"version-fiddled": { | 		"version-fiddled": { | ||||||
| 			Value: &AuditEvent{ | 			Value: &AuditEvent{ | ||||||
| @@ -146,7 +146,7 @@ func TestAuditEvent_Validate(t *testing.T) { | |||||||
| 				Data:      nil, | 				Data:      nil, | ||||||
| 			}, | 			}, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(AuditEvent).validate: event version unsupported: invalid parameter", | 			ExpectedErrorMessage: "event version unsupported: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"subtype-fiddled": { | 		"subtype-fiddled": { | ||||||
| 			Value: &AuditEvent{ | 			Value: &AuditEvent{ | ||||||
| @@ -157,7 +157,7 @@ func TestAuditEvent_Validate(t *testing.T) { | |||||||
| 				Data:      nil, | 				Data:      nil, | ||||||
| 			}, | 			}, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(AuditEvent).validate: audit.(subtype).validate: 'moon' is not a valid event subtype: invalid parameter", | 			ExpectedErrorMessage: "invalid event subtype \"moon\": invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"default-time": { | 		"default-time": { | ||||||
| 			Value: &AuditEvent{ | 			Value: &AuditEvent{ | ||||||
| @@ -168,7 +168,7 @@ func TestAuditEvent_Validate(t *testing.T) { | |||||||
| 				Data:      nil, | 				Data:      nil, | ||||||
| 			}, | 			}, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(AuditEvent).validate: event timestamp cannot be the zero time instant: invalid parameter", | 			ExpectedErrorMessage: "event timestamp cannot be the zero time instant: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"valid": { | 		"valid": { | ||||||
| 			Value: &AuditEvent{ | 			Value: &AuditEvent{ | ||||||
| @@ -212,12 +212,12 @@ func TestAuditEvent_Validate_Subtype(t *testing.T) { | |||||||
| 		"empty": { | 		"empty": { | ||||||
| 			Value:                "", | 			Value:                "", | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(subtype).validate: '' is not a valid event subtype: invalid parameter", | 			ExpectedErrorMessage: "invalid event subtype \"\": invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"unsupported": { | 		"unsupported": { | ||||||
| 			Value:                "foo", | 			Value:                "foo", | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(subtype).validate: 'foo' is not a valid event subtype: invalid parameter", | 			ExpectedErrorMessage: "invalid event subtype \"foo\": invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"request": { | 		"request": { | ||||||
| 			Value:           "AuditRequest", | 			Value:           "AuditRequest", | ||||||
| @@ -259,12 +259,12 @@ func TestAuditEvent_Validate_Format(t *testing.T) { | |||||||
| 		"empty": { | 		"empty": { | ||||||
| 			Value:                "", | 			Value:                "", | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(format).validate: '' is not a valid format: invalid parameter", | 			ExpectedErrorMessage: "invalid format \"\": invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"unsupported": { | 		"unsupported": { | ||||||
| 			Value:                "foo", | 			Value:                "foo", | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(format).validate: 'foo' is not a valid format: invalid parameter", | 			ExpectedErrorMessage: "invalid format \"foo\": invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"json": { | 		"json": { | ||||||
| 			Value:           "json", | 			Value:           "json", | ||||||
| @@ -378,3 +378,69 @@ func TestAuditEvent_formattedTime(t *testing.T) { | |||||||
| 	require.NotNil(t, a) | 	require.NotNil(t, a) | ||||||
| 	require.Equal(t, "2024-03-22T10:00:05.00000001Z", a.formattedTime()) | 	require.Equal(t, "2024-03-22T10:00:05.00000001Z", a.formattedTime()) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // TestEvent_IsValidFormat ensures that we can correctly determine valid and | ||||||
|  | // invalid formats. | ||||||
|  | func TestEvent_IsValidFormat(t *testing.T) { | ||||||
|  | 	t.Parallel() | ||||||
|  |  | ||||||
|  | 	tests := map[string]struct { | ||||||
|  | 		input    string | ||||||
|  | 		expected bool | ||||||
|  | 	}{ | ||||||
|  | 		"empty": { | ||||||
|  | 			input:    "", | ||||||
|  | 			expected: false, | ||||||
|  | 		}, | ||||||
|  | 		"whitespace": { | ||||||
|  | 			input:    "     ", | ||||||
|  | 			expected: false, | ||||||
|  | 		}, | ||||||
|  | 		"invalid-test": { | ||||||
|  | 			input:    "test", | ||||||
|  | 			expected: false, | ||||||
|  | 		}, | ||||||
|  | 		"valid-json": { | ||||||
|  | 			input:    "json", | ||||||
|  | 			expected: true, | ||||||
|  | 		}, | ||||||
|  | 		"upper-json": { | ||||||
|  | 			input:    "JSON", | ||||||
|  | 			expected: true, | ||||||
|  | 		}, | ||||||
|  | 		"mixed-json": { | ||||||
|  | 			input:    "Json", | ||||||
|  | 			expected: true, | ||||||
|  | 		}, | ||||||
|  | 		"spacey-json": { | ||||||
|  | 			input:    "  json  ", | ||||||
|  | 			expected: true, | ||||||
|  | 		}, | ||||||
|  | 		"valid-jsonx": { | ||||||
|  | 			input:    "jsonx", | ||||||
|  | 			expected: true, | ||||||
|  | 		}, | ||||||
|  | 		"upper-jsonx": { | ||||||
|  | 			input:    "JSONX", | ||||||
|  | 			expected: true, | ||||||
|  | 		}, | ||||||
|  | 		"mixed-jsonx": { | ||||||
|  | 			input:    "JsonX", | ||||||
|  | 			expected: true, | ||||||
|  | 		}, | ||||||
|  | 		"spacey-jsonx": { | ||||||
|  | 			input:    "  jsonx  ", | ||||||
|  | 			expected: true, | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for name, tc := range tests { | ||||||
|  | 		name := name | ||||||
|  | 		tc := tc | ||||||
|  | 		t.Run(name, func(t *testing.T) { | ||||||
|  | 			t.Parallel() | ||||||
|  | 			res := IsValidFormat(tc.input) | ||||||
|  | 			require.Equal(t, tc.expected, res) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ func ProcessManual(ctx context.Context, data *logical.LogInput, ids []eventlogge | |||||||
|  |  | ||||||
| 	// Create an eventlogger event with the audit event as the payload. | 	// Create an eventlogger event with the audit event as the payload. | ||||||
| 	e := &eventlogger.Event{ | 	e := &eventlogger.Event{ | ||||||
| 		Type:      eventlogger.EventType(event.AuditType.String()), | 		Type:      event.AuditType.AsEventType(), | ||||||
| 		CreatedAt: time.Now(), | 		CreatedAt: time.Now(), | ||||||
| 		Formatted: make(map[string][]byte), | 		Formatted: make(map[string][]byte), | ||||||
| 		Payload:   a, | 		Payload:   a, | ||||||
|   | |||||||
| @@ -104,7 +104,7 @@ func WithSubtype(s string) Option { | |||||||
| // WithFormat provides an Option to represent event format. | // WithFormat provides an Option to represent event format. | ||||||
| func WithFormat(f string) Option { | func WithFormat(f string) Option { | ||||||
| 	return func(o *options) error { | 	return func(o *options) error { | ||||||
| 		f := strings.TrimSpace(f) | 		f := strings.TrimSpace(strings.ToLower(f)) | ||||||
| 		if f == "" { | 		if f == "" { | ||||||
| 			// Return early, we won't attempt to apply this option if its empty. | 			// Return early, we won't attempt to apply this option if its empty. | ||||||
| 			return nil | 			return nil | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ func TestOptions_WithFormat(t *testing.T) { | |||||||
| 		"invalid-test": { | 		"invalid-test": { | ||||||
| 			Value:                "test", | 			Value:                "test", | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "audit.(format).validate: 'test' is not a valid format: invalid parameter", | 			ExpectedErrorMessage: "invalid format \"test\": invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"valid-json": { | 		"valid-json": { | ||||||
| 			Value:           "json", | 			Value:           "json", | ||||||
|   | |||||||
| @@ -11,7 +11,6 @@ import ( | |||||||
|  |  | ||||||
| 	"github.com/armon/go-metrics" | 	"github.com/armon/go-metrics" | ||||||
| 	"github.com/hashicorp/eventlogger" | 	"github.com/hashicorp/eventlogger" | ||||||
| 	"github.com/hashicorp/vault/internal/observability/event" |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var _ eventlogger.Node = (*SinkMetricTimer)(nil) | var _ eventlogger.Node = (*SinkMetricTimer)(nil) | ||||||
| @@ -29,19 +28,17 @@ type SinkMetricTimer struct { | |||||||
| // NewSinkMetricTimer should be used to create the SinkMetricTimer. | // NewSinkMetricTimer should be used to create the SinkMetricTimer. | ||||||
| // It expects that an eventlogger.NodeTypeSink should be supplied as the sink. | // It expects that an eventlogger.NodeTypeSink should be supplied as the sink. | ||||||
| func NewSinkMetricTimer(name string, sink eventlogger.Node) (*SinkMetricTimer, error) { | func NewSinkMetricTimer(name string, sink eventlogger.Node) (*SinkMetricTimer, error) { | ||||||
| 	const op = "audit.NewSinkMetricTimer" |  | ||||||
|  |  | ||||||
| 	name = strings.TrimSpace(name) | 	name = strings.TrimSpace(name) | ||||||
| 	if name == "" { | 	if name == "" { | ||||||
| 		return nil, fmt.Errorf("%s: name is required: %w", op, event.ErrInvalidParameter) | 		return nil, fmt.Errorf("name is required: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if sink == nil || reflect.ValueOf(sink).IsNil() { | 	if sink == nil || reflect.ValueOf(sink).IsNil() { | ||||||
| 		return nil, fmt.Errorf("%s: sink node is required: %w", op, event.ErrInvalidParameter) | 		return nil, fmt.Errorf("sink node is required: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if sink.Type() != eventlogger.NodeTypeSink { | 	if sink.Type() != eventlogger.NodeTypeSink { | ||||||
| 		return nil, fmt.Errorf("%s: sink node must be of type 'sink': %w", op, event.ErrInvalidParameter) | 		return nil, fmt.Errorf("sink node must be of type 'sink': %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return &SinkMetricTimer{ | 	return &SinkMetricTimer{ | ||||||
|   | |||||||
| @@ -30,19 +30,19 @@ func TestNewSinkMetricTimer(t *testing.T) { | |||||||
| 		"no-name": { | 		"no-name": { | ||||||
| 			name:                 "", | 			name:                 "", | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "audit.NewSinkMetricTimer: name is required: invalid parameter", | 			expectedErrorMessage: "name is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"no-node": { | 		"no-node": { | ||||||
| 			name:                 "foo", | 			name:                 "foo", | ||||||
| 			node:                 nil, | 			node:                 nil, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "audit.NewSinkMetricTimer: sink node is required: invalid parameter", | 			expectedErrorMessage: "sink node is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"bad-node": { | 		"bad-node": { | ||||||
| 			name:                 "foo", | 			name:                 "foo", | ||||||
| 			node:                 &EntryFormatter{}, | 			node:                 &EntryFormatter{}, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "audit.NewSinkMetricTimer: sink node must be of type 'sink': invalid parameter", | 			expectedErrorMessage: "sink node must be of type 'sink': invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -66,47 +66,6 @@ type HeaderFormatter interface { | |||||||
| 	ApplyConfig(context.Context, map[string][]string, Salter) (map[string][]string, error) | 	ApplyConfig(context.Context, map[string][]string, Salter) (map[string][]string, error) | ||||||
| } | } | ||||||
|  |  | ||||||
| // FormatterConfig is used to provide basic configuration to a formatter. |  | ||||||
| // Use NewFormatterConfig to initialize the FormatterConfig struct. |  | ||||||
| type FormatterConfig struct { |  | ||||||
| 	Raw          bool |  | ||||||
| 	HMACAccessor bool |  | ||||||
|  |  | ||||||
| 	// Vault lacks pagination in its APIs. As a result, certain list operations can return **very** large responses. |  | ||||||
| 	// The user's chosen audit sinks may experience difficulty consuming audit records that swell to tens of megabytes |  | ||||||
| 	// of JSON. The responses of list operations are typically not very interesting, as they are mostly lists of keys, |  | ||||||
| 	// or, even when they include a "key_info" field, are not returning confidential information. They become even less |  | ||||||
| 	// interesting once HMAC-ed by the audit system. |  | ||||||
| 	// |  | ||||||
| 	// Some example Vault "list" operations that are prone to becoming very large in an active Vault installation are: |  | ||||||
| 	//   auth/token/accessors/ |  | ||||||
| 	//   identity/entity/id/ |  | ||||||
| 	//   identity/entity-alias/id/ |  | ||||||
| 	//   pki/certs/ |  | ||||||
| 	// |  | ||||||
| 	// This option exists to provide such users with the option to have response data elided from audit logs, only when |  | ||||||
| 	// the operation type is "list". For added safety, the elision only applies to the "keys" and "key_info" fields |  | ||||||
| 	// within the response data - these are conventionally the only fields present in a list response - see |  | ||||||
| 	// logical.ListResponse, and logical.ListResponseWithInfo. However, other fields are technically possible if a |  | ||||||
| 	// plugin author writes unusual code, and these will be preserved in the audit log even with this option enabled. |  | ||||||
| 	// The elision replaces the values of the "keys" and "key_info" fields with an integer count of the number of |  | ||||||
| 	// entries. This allows even the elided audit logs to still be useful for answering questions like |  | ||||||
| 	// "Was any data returned?" or "How many records were listed?". |  | ||||||
| 	ElideListResponses bool |  | ||||||
|  |  | ||||||
| 	// This should only ever be used in a testing context |  | ||||||
| 	OmitTime bool |  | ||||||
|  |  | ||||||
| 	// The required/target format for the event (supported: JSONFormat and JSONxFormat). |  | ||||||
| 	RequiredFormat format |  | ||||||
|  |  | ||||||
| 	// headerFormatter specifies the formatter used for headers that existing in any incoming audit request. |  | ||||||
| 	headerFormatter HeaderFormatter |  | ||||||
|  |  | ||||||
| 	// Prefix specifies a Prefix that should be prepended to any formatted request or response before serialization. |  | ||||||
| 	Prefix string |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // RequestEntry is the structure of a request audit log entry. | // RequestEntry is the structure of a request audit log entry. | ||||||
| type RequestEntry struct { | type RequestEntry struct { | ||||||
| 	Auth          *Auth    `json:"auth,omitempty"` | 	Auth          *Auth    `json:"auth,omitempty"` | ||||||
|   | |||||||
| @@ -45,18 +45,20 @@ type Backend struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| func Factory(_ context.Context, conf *audit.BackendConfig, headersConfig audit.HeaderFormatter) (audit.Backend, error) { | func Factory(_ context.Context, conf *audit.BackendConfig, headersConfig audit.HeaderFormatter) (audit.Backend, error) { | ||||||
| 	const op = "file.Factory" |  | ||||||
|  |  | ||||||
| 	if conf.SaltConfig == nil { | 	if conf.SaltConfig == nil { | ||||||
| 		return nil, fmt.Errorf("%s: nil salt config", op) | 		return nil, fmt.Errorf("nil salt config: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if conf.SaltView == nil { | 	if conf.SaltView == nil { | ||||||
| 		return nil, fmt.Errorf("%s: nil salt view", op) | 		return nil, fmt.Errorf("nil salt view: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if conf.Logger == nil || reflect.ValueOf(conf.Logger).IsNil() { | 	if conf.Logger == nil || reflect.ValueOf(conf.Logger).IsNil() { | ||||||
| 		return nil, fmt.Errorf("%s: nil logger", op) | 		return nil, fmt.Errorf("nil logger: %w", audit.ErrInvalidParameter) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if conf.MountPath == "" { | ||||||
|  | 		return nil, fmt.Errorf("mount path cannot be empty: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// The config options 'fallback' and 'filter' are mutually exclusive, a fallback | 	// The config options 'fallback' and 'filter' are mutually exclusive, a fallback | ||||||
| @@ -66,12 +68,12 @@ func Factory(_ context.Context, conf *audit.BackendConfig, headersConfig audit.H | |||||||
| 	if fallbackRaw, ok := conf.Config["fallback"]; ok { | 	if fallbackRaw, ok := conf.Config["fallback"]; ok { | ||||||
| 		fallback, err = parseutil.ParseBool(fallbackRaw) | 		fallback, err = parseutil.ParseBool(fallbackRaw) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("%s: unable to parse 'fallback': %w", op, err) | 			return nil, fmt.Errorf("unable to parse 'fallback': %w", audit.ErrExternalOptions) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if _, ok := conf.Config["filter"]; ok && fallback { | 	if _, ok := conf.Config["filter"]; ok && fallback { | ||||||
| 		return nil, fmt.Errorf("%s: cannot configure a fallback device with a filter: %w", op, event.ErrInvalidParameter) | 		return nil, fmt.Errorf("cannot configure a fallback device with a filter: %w", audit.ErrExternalOptions) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Get file path from config or fall back to the old option name ('path') for compatibility | 	// Get file path from config or fall back to the old option name ('path') for compatibility | ||||||
| @@ -82,7 +84,7 @@ func Factory(_ context.Context, conf *audit.BackendConfig, headersConfig audit.H | |||||||
| 	} else if p, ok = conf.Config["path"]; ok { | 	} else if p, ok = conf.Config["path"]; ok { | ||||||
| 		filePath = p | 		filePath = p | ||||||
| 	} else { | 	} else { | ||||||
| 		return nil, fmt.Errorf("%s: file_path is required", op) | 		return nil, fmt.Errorf("file_path is required: %w", audit.ErrExternalOptions) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// normalize file path if configured for stdout | 	// normalize file path if configured for stdout | ||||||
| @@ -93,6 +95,11 @@ func Factory(_ context.Context, conf *audit.BackendConfig, headersConfig audit.H | |||||||
| 		filePath = discard | 		filePath = discard | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	cfg, err := newFormatterConfig(headersConfig, conf.Config) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	b := &Backend{ | 	b := &Backend{ | ||||||
| 		fallback:   fallback, | 		fallback:   fallback, | ||||||
| 		name:       conf.MountPath, | 		name:       conf.MountPath, | ||||||
| @@ -109,22 +116,17 @@ func Factory(_ context.Context, conf *audit.BackendConfig, headersConfig audit.H | |||||||
|  |  | ||||||
| 	err = b.configureFilterNode(conf.Config["filter"]) | 	err = b.configureFilterNode(conf.Config["filter"]) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error configuring filter node: %w", op, err) | 		return nil, err | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	cfg, err := newFormatterConfig(headersConfig, conf.Config) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("%s: failed to create formatter config: %w", op, err) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err = b.configureFormatterNode(conf.MountPath, cfg, conf.Logger) | 	err = b.configureFormatterNode(conf.MountPath, cfg, conf.Logger) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error configuring formatter node: %w", op, err) | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err = b.configureSinkNode(conf.MountPath, filePath, conf.Config["mode"], cfg.RequiredFormat.String()) | 	err = b.configureSinkNode(conf.MountPath, filePath, conf.Config["mode"], cfg.RequiredFormat.String()) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error configuring sink node: %w", op, err) | 		return nil, fmt.Errorf("error configuring sink node: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return b, nil | 	return b, nil | ||||||
| @@ -181,11 +183,13 @@ func (b *Backend) Invalidate(_ context.Context) { | |||||||
| // newFormatterConfig creates the configuration required by a formatter node using | // newFormatterConfig creates the configuration required by a formatter node using | ||||||
| // the config map supplied to the factory. | // the config map supplied to the factory. | ||||||
| func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string]string) (audit.FormatterConfig, error) { | func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string]string) (audit.FormatterConfig, error) { | ||||||
| 	const op = "file.newFormatterConfig" |  | ||||||
|  |  | ||||||
| 	var opts []audit.Option | 	var opts []audit.Option | ||||||
|  |  | ||||||
| 	if format, ok := config["format"]; ok { | 	if format, ok := config["format"]; ok { | ||||||
|  | 		if !audit.IsValidFormat(format) { | ||||||
|  | 			return audit.FormatterConfig{}, fmt.Errorf("unsupported 'format': %w", audit.ErrExternalOptions) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		opts = append(opts, audit.WithFormat(format)) | 		opts = append(opts, audit.WithFormat(format)) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -193,7 +197,7 @@ func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string | |||||||
| 	if hmacAccessorRaw, ok := config["hmac_accessor"]; ok { | 	if hmacAccessorRaw, ok := config["hmac_accessor"]; ok { | ||||||
| 		v, err := strconv.ParseBool(hmacAccessorRaw) | 		v, err := strconv.ParseBool(hmacAccessorRaw) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return audit.FormatterConfig{}, fmt.Errorf("%s: unable to parse 'hmac_accessor': %w", op, err) | 			return audit.FormatterConfig{}, fmt.Errorf("unable to parse 'hmac_accessor': %w", audit.ErrExternalOptions) | ||||||
| 		} | 		} | ||||||
| 		opts = append(opts, audit.WithHMACAccessor(v)) | 		opts = append(opts, audit.WithHMACAccessor(v)) | ||||||
| 	} | 	} | ||||||
| @@ -202,7 +206,7 @@ func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string | |||||||
| 	if raw, ok := config["log_raw"]; ok { | 	if raw, ok := config["log_raw"]; ok { | ||||||
| 		v, err := strconv.ParseBool(raw) | 		v, err := strconv.ParseBool(raw) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return audit.FormatterConfig{}, fmt.Errorf("%s: unable to parse 'log_raw': %w", op, err) | 			return audit.FormatterConfig{}, fmt.Errorf("unable to parse 'log_raw: %w", audit.ErrExternalOptions) | ||||||
| 		} | 		} | ||||||
| 		opts = append(opts, audit.WithRaw(v)) | 		opts = append(opts, audit.WithRaw(v)) | ||||||
| 	} | 	} | ||||||
| @@ -210,7 +214,7 @@ func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string | |||||||
| 	if elideListResponsesRaw, ok := config["elide_list_responses"]; ok { | 	if elideListResponsesRaw, ok := config["elide_list_responses"]; ok { | ||||||
| 		v, err := strconv.ParseBool(elideListResponsesRaw) | 		v, err := strconv.ParseBool(elideListResponsesRaw) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return audit.FormatterConfig{}, fmt.Errorf("%s: unable to parse 'elide_list_responses': %w", op, err) | 			return audit.FormatterConfig{}, fmt.Errorf("unable to parse 'elide_list_responses': %w", audit.ErrExternalOptions) | ||||||
| 		} | 		} | ||||||
| 		opts = append(opts, audit.WithElision(v)) | 		opts = append(opts, audit.WithElision(v)) | ||||||
| 	} | 	} | ||||||
| @@ -224,16 +228,14 @@ func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string | |||||||
|  |  | ||||||
| // configureFormatterNode is used to configure a formatter node and associated ID on the Backend. | // configureFormatterNode is used to configure a formatter node and associated ID on the Backend. | ||||||
| func (b *Backend) configureFormatterNode(name string, formatConfig audit.FormatterConfig, logger hclog.Logger) error { | func (b *Backend) configureFormatterNode(name string, formatConfig audit.FormatterConfig, logger hclog.Logger) error { | ||||||
| 	const op = "file.(Backend).configureFormatterNode" |  | ||||||
|  |  | ||||||
| 	formatterNodeID, err := event.GenerateNodeID() | 	formatterNodeID, err := event.GenerateNodeID() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: error generating random NodeID for formatter node: %w", op, err) | 		return fmt.Errorf("error generating random NodeID for formatter node: %w: %w", audit.ErrInternal, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	formatterNode, err := audit.NewEntryFormatter(name, formatConfig, b, logger) | 	formatterNode, err := audit.NewEntryFormatter(name, formatConfig, b, logger) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: error creating formatter: %w", op, err) | 		return fmt.Errorf("error creating formatter: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	b.nodeIDList = append(b.nodeIDList, formatterNodeID) | 	b.nodeIDList = append(b.nodeIDList, formatterNodeID) | ||||||
| @@ -244,26 +246,24 @@ func (b *Backend) configureFormatterNode(name string, formatConfig audit.Formatt | |||||||
|  |  | ||||||
| // configureSinkNode is used to configure a sink node and associated ID on the Backend. | // configureSinkNode is used to configure a sink node and associated ID on the Backend. | ||||||
| func (b *Backend) configureSinkNode(name string, filePath string, mode string, format string) error { | func (b *Backend) configureSinkNode(name string, filePath string, mode string, format string) error { | ||||||
| 	const op = "file.(Backend).configureSinkNode" |  | ||||||
|  |  | ||||||
| 	name = strings.TrimSpace(name) | 	name = strings.TrimSpace(name) | ||||||
| 	if name == "" { | 	if name == "" { | ||||||
| 		return fmt.Errorf("%s: name is required: %w", op, event.ErrInvalidParameter) | 		return fmt.Errorf("name is required: %w", audit.ErrExternalOptions) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	filePath = strings.TrimSpace(filePath) | 	filePath = strings.TrimSpace(filePath) | ||||||
| 	if filePath == "" { | 	if filePath == "" { | ||||||
| 		return fmt.Errorf("%s: file path is required: %w", op, event.ErrInvalidParameter) | 		return fmt.Errorf("file path is required: %w", audit.ErrExternalOptions) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	format = strings.TrimSpace(format) | 	format = strings.TrimSpace(format) | ||||||
| 	if format == "" { | 	if format == "" { | ||||||
| 		return fmt.Errorf("%s: format is required: %w", op, event.ErrInvalidParameter) | 		return fmt.Errorf("format is required: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	sinkNodeID, err := event.GenerateNodeID() | 	sinkNodeID, err := event.GenerateNodeID() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: error generating random NodeID for sink node: %w", op, err) | 		return fmt.Errorf("error generating random NodeID for sink node: %w: %w", audit.ErrInternal, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// normalize file path if configured for stdout or discard | 	// normalize file path if configured for stdout or discard | ||||||
| @@ -290,13 +290,13 @@ func (b *Backend) configureSinkNode(name string, filePath string, mode string, f | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: file sink creation failed for path %q: %w", op, filePath, err) | 		return fmt.Errorf("file sink creation failed for path %q: %w", filePath, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Wrap the sink node with metrics middleware | 	// Wrap the sink node with metrics middleware | ||||||
| 	sinkMetricTimer, err := audit.NewSinkMetricTimer(sinkName, sinkNode) | 	sinkMetricTimer, err := audit.NewSinkMetricTimer(sinkName, sinkNode) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to add timing metrics to sink for path %q: %w", op, filePath, err) | 		return fmt.Errorf("unable to add timing metrics to sink for path %q: %w", filePath, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Decide what kind of labels we want and wrap the sink node inside a metrics counter. | 	// Decide what kind of labels we want and wrap the sink node inside a metrics counter. | ||||||
| @@ -310,7 +310,7 @@ func (b *Backend) configureSinkNode(name string, filePath string, mode string, f | |||||||
|  |  | ||||||
| 	sinkMetricCounter, err := event.NewMetricsCounter(sinkName, sinkMetricTimer, metricLabeler) | 	sinkMetricCounter, err := event.NewMetricsCounter(sinkName, sinkMetricTimer, metricLabeler) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to add counting metrics to sink for path %q: %w", op, filePath, err) | 		return fmt.Errorf("unable to add counting metrics to sink for path %q: %w", filePath, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	b.nodeIDList = append(b.nodeIDList, sinkNodeID) | 	b.nodeIDList = append(b.nodeIDList, sinkNodeID) | ||||||
| @@ -336,7 +336,7 @@ func (b *Backend) NodeIDs() []eventlogger.NodeID { | |||||||
|  |  | ||||||
| // EventType returns the event type for the backend. | // EventType returns the event type for the backend. | ||||||
| func (b *Backend) EventType() eventlogger.EventType { | func (b *Backend) EventType() eventlogger.EventType { | ||||||
| 	return eventlogger.EventType(event.AuditType.String()) | 	return event.AuditType.AsEventType() | ||||||
| } | } | ||||||
|  |  | ||||||
| // HasFiltering determines if the first node for the pipeline is an eventlogger.NodeTypeFilter. | // HasFiltering determines if the first node for the pipeline is an eventlogger.NodeTypeFilter. | ||||||
|   | |||||||
| @@ -193,7 +193,7 @@ func TestBackend_newFormatterConfig(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 			want:            audit.FormatterConfig{}, | 			want:            audit.FormatterConfig{}, | ||||||
| 			wantErr:         true, | 			wantErr:         true, | ||||||
| 			expectedMessage: "audit.NewFormatterConfig: error applying options: audit.(format).validate: 'squiggly' is not a valid format: invalid parameter", | 			expectedMessage: "unsupported 'format': invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"invalid-hmac-accessor": { | 		"invalid-hmac-accessor": { | ||||||
| 			config: map[string]string{ | 			config: map[string]string{ | ||||||
| @@ -202,7 +202,7 @@ func TestBackend_newFormatterConfig(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 			want:            audit.FormatterConfig{}, | 			want:            audit.FormatterConfig{}, | ||||||
| 			wantErr:         true, | 			wantErr:         true, | ||||||
| 			expectedMessage: "file.newFormatterConfig: unable to parse 'hmac_accessor': strconv.ParseBool: parsing \"maybe\": invalid syntax", | 			expectedMessage: "unable to parse 'hmac_accessor': invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"invalid-log-raw": { | 		"invalid-log-raw": { | ||||||
| 			config: map[string]string{ | 			config: map[string]string{ | ||||||
| @@ -212,7 +212,7 @@ func TestBackend_newFormatterConfig(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 			want:            audit.FormatterConfig{}, | 			want:            audit.FormatterConfig{}, | ||||||
| 			wantErr:         true, | 			wantErr:         true, | ||||||
| 			expectedMessage: "file.newFormatterConfig: unable to parse 'log_raw': strconv.ParseBool: parsing \"maybe\": invalid syntax", | 			expectedMessage: "unable to parse 'log_raw: invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"invalid-elide-bool": { | 		"invalid-elide-bool": { | ||||||
| 			config: map[string]string{ | 			config: map[string]string{ | ||||||
| @@ -223,7 +223,7 @@ func TestBackend_newFormatterConfig(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 			want:            audit.FormatterConfig{}, | 			want:            audit.FormatterConfig{}, | ||||||
| 			wantErr:         true, | 			wantErr:         true, | ||||||
| 			expectedMessage: "file.newFormatterConfig: unable to parse 'elide_list_responses': strconv.ParseBool: parsing \"maybe\": invalid syntax", | 			expectedMessage: "unable to parse 'elide_list_responses': invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"prefix": { | 		"prefix": { | ||||||
| 			config: map[string]string{ | 			config: map[string]string{ | ||||||
| @@ -300,24 +300,24 @@ func TestBackend_configureSinkNode(t *testing.T) { | |||||||
| 		"name-empty": { | 		"name-empty": { | ||||||
| 			name:           "", | 			name:           "", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "file.(Backend).configureSinkNode: name is required: invalid parameter", | 			expectedErrMsg: "name is required: invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"name-whitespace": { | 		"name-whitespace": { | ||||||
| 			name:           "   ", | 			name:           "   ", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "file.(Backend).configureSinkNode: name is required: invalid parameter", | 			expectedErrMsg: "name is required: invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"filePath-empty": { | 		"filePath-empty": { | ||||||
| 			name:           "foo", | 			name:           "foo", | ||||||
| 			filePath:       "", | 			filePath:       "", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "file.(Backend).configureSinkNode: file path is required: invalid parameter", | 			expectedErrMsg: "file path is required: invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"filePath-whitespace": { | 		"filePath-whitespace": { | ||||||
| 			name:           "foo", | 			name:           "foo", | ||||||
| 			filePath:       "   ", | 			filePath:       "   ", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "file.(Backend).configureSinkNode: file path is required: invalid parameter", | 			expectedErrMsg: "file path is required: invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"filePath-stdout-lower": { | 		"filePath-stdout-lower": { | ||||||
| 			name:         "foo", | 			name:         "foo", | ||||||
| @@ -360,14 +360,14 @@ func TestBackend_configureSinkNode(t *testing.T) { | |||||||
| 			filePath:       "/tmp/", | 			filePath:       "/tmp/", | ||||||
| 			format:         "", | 			format:         "", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "file.(Backend).configureSinkNode: format is required: invalid parameter", | 			expectedErrMsg: "format is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"format-whitespace": { | 		"format-whitespace": { | ||||||
| 			name:           "foo", | 			name:           "foo", | ||||||
| 			filePath:       "/tmp/", | 			filePath:       "/tmp/", | ||||||
| 			format:         "   ", | 			format:         "   ", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "file.(Backend).configureSinkNode: format is required: invalid parameter", | 			expectedErrMsg: "format is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"filePath-weird-with-mode-zero": { | 		"filePath-weird-with-mode-zero": { | ||||||
| 			name:           "foo", | 			name:           "foo", | ||||||
| @@ -375,7 +375,7 @@ func TestBackend_configureSinkNode(t *testing.T) { | |||||||
| 			format:         "json", | 			format:         "json", | ||||||
| 			mode:           "0", | 			mode:           "0", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "file.(Backend).configureSinkNode: file sink creation failed for path \"/tmp/qwerty\": event.NewFileSink: unable to determine existing file mode: stat /tmp/qwerty: no such file or directory", | 			expectedErrMsg: "file sink creation failed for path \"/tmp/qwerty\": unable to determine existing file mode: stat /tmp/qwerty: no such file or directory", | ||||||
| 		}, | 		}, | ||||||
| 		"happy": { | 		"happy": { | ||||||
| 			name:         "foo", | 			name:         "foo", | ||||||
| @@ -437,14 +437,14 @@ func TestBackend_Factory_Conf(t *testing.T) { | |||||||
| 				SaltConfig: nil, | 				SaltConfig: nil, | ||||||
| 			}, | 			}, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "file.Factory: nil salt config", | 			expectedErrorMessage: "nil salt config: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"nil-salt-view": { | 		"nil-salt-view": { | ||||||
| 			backendConfig: &audit.BackendConfig{ | 			backendConfig: &audit.BackendConfig{ | ||||||
| 				SaltConfig: &salt.Config{}, | 				SaltConfig: &salt.Config{}, | ||||||
| 			}, | 			}, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "file.Factory: nil salt view", | 			expectedErrorMessage: "nil salt view: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"nil-logger": { | 		"nil-logger": { | ||||||
| 			backendConfig: &audit.BackendConfig{ | 			backendConfig: &audit.BackendConfig{ | ||||||
| @@ -454,7 +454,7 @@ func TestBackend_Factory_Conf(t *testing.T) { | |||||||
| 				Logger:     nil, | 				Logger:     nil, | ||||||
| 			}, | 			}, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "file.Factory: nil logger", | 			expectedErrorMessage: "nil logger: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"fallback-device-with-filter": { | 		"fallback-device-with-filter": { | ||||||
| 			backendConfig: &audit.BackendConfig{ | 			backendConfig: &audit.BackendConfig{ | ||||||
| @@ -469,7 +469,7 @@ func TestBackend_Factory_Conf(t *testing.T) { | |||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "file.Factory: cannot configure a fallback device with a filter: invalid parameter", | 			expectedErrorMessage: "cannot configure a fallback device with a filter: invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"non-fallback-device-with-filter": { | 		"non-fallback-device-with-filter": { | ||||||
| 			backendConfig: &audit.BackendConfig{ | 			backendConfig: &audit.BackendConfig{ | ||||||
|   | |||||||
| @@ -35,23 +35,24 @@ type Backend struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| func Factory(_ context.Context, conf *audit.BackendConfig, headersConfig audit.HeaderFormatter) (audit.Backend, error) { | func Factory(_ context.Context, conf *audit.BackendConfig, headersConfig audit.HeaderFormatter) (audit.Backend, error) { | ||||||
| 	const op = "socket.Factory" |  | ||||||
|  |  | ||||||
| 	if conf.SaltConfig == nil { | 	if conf.SaltConfig == nil { | ||||||
| 		return nil, fmt.Errorf("%s: nil salt config", op) | 		return nil, fmt.Errorf("nil salt config: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if conf.SaltView == nil { | 	if conf.SaltView == nil { | ||||||
| 		return nil, fmt.Errorf("%s: nil salt view", op) | 		return nil, fmt.Errorf("nil salt view: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if conf.Logger == nil || reflect.ValueOf(conf.Logger).IsNil() { | 	if conf.Logger == nil || reflect.ValueOf(conf.Logger).IsNil() { | ||||||
| 		return nil, fmt.Errorf("%s: nil logger", op) | 		return nil, fmt.Errorf("nil logger: %w", audit.ErrInvalidParameter) | ||||||
|  | 	} | ||||||
|  | 	if conf.MountPath == "" { | ||||||
|  | 		return nil, fmt.Errorf("mount path cannot be empty: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	address, ok := conf.Config["address"] | 	address, ok := conf.Config["address"] | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return nil, fmt.Errorf("%s: address is required", op) | 		return nil, fmt.Errorf("address is required: %w", audit.ErrExternalOptions) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	socketType, ok := conf.Config["socket_type"] | 	socketType, ok := conf.Config["socket_type"] | ||||||
| @@ -64,19 +65,33 @@ func Factory(_ context.Context, conf *audit.BackendConfig, headersConfig audit.H | |||||||
| 		writeDeadline = "2s" | 		writeDeadline = "2s" | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	sinkOpts := []event.Option{ | ||||||
|  | 		event.WithSocketType(socketType), | ||||||
|  | 		event.WithMaxDuration(writeDeadline), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err := event.ValidateOptions(sinkOpts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// The config options 'fallback' and 'filter' are mutually exclusive, a fallback | 	// The config options 'fallback' and 'filter' are mutually exclusive, a fallback | ||||||
| 	// device catches everything, so it cannot be allowed to filter. | 	// device catches everything, so it cannot be allowed to filter. | ||||||
| 	var fallback bool | 	var fallback bool | ||||||
| 	var err error |  | ||||||
| 	if fallbackRaw, ok := conf.Config["fallback"]; ok { | 	if fallbackRaw, ok := conf.Config["fallback"]; ok { | ||||||
| 		fallback, err = parseutil.ParseBool(fallbackRaw) | 		fallback, err = parseutil.ParseBool(fallbackRaw) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("%s: unable to parse 'fallback': %w", op, err) | 			return nil, fmt.Errorf("unable to parse 'fallback': %w", audit.ErrExternalOptions) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if _, ok := conf.Config["filter"]; ok && fallback { | 	if _, ok := conf.Config["filter"]; ok && fallback { | ||||||
| 		return nil, fmt.Errorf("%s: cannot configure a fallback device with a filter: %w", op, event.ErrInvalidParameter) | 		return nil, fmt.Errorf("cannot configure a fallback device with a filter: %w", audit.ErrExternalOptions) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	cfg, err := newFormatterConfig(headersConfig, conf.Config) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	b := &Backend{ | 	b := &Backend{ | ||||||
| @@ -90,27 +105,17 @@ func Factory(_ context.Context, conf *audit.BackendConfig, headersConfig audit.H | |||||||
|  |  | ||||||
| 	err = b.configureFilterNode(conf.Config["filter"]) | 	err = b.configureFilterNode(conf.Config["filter"]) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error configuring filter node: %w", op, err) | 		return nil, err | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	cfg, err := newFormatterConfig(headersConfig, conf.Config) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("%s: failed to create formatter config: %w", op, err) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err = b.configureFormatterNode(conf.MountPath, cfg, conf.Logger) | 	err = b.configureFormatterNode(conf.MountPath, cfg, conf.Logger) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error configuring formatter node: %w", op, err) | 		return nil, err | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	sinkOpts := []event.Option{ |  | ||||||
| 		event.WithSocketType(socketType), |  | ||||||
| 		event.WithMaxDuration(writeDeadline), |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err = b.configureSinkNode(conf.MountPath, address, cfg.RequiredFormat.String(), sinkOpts...) | 	err = b.configureSinkNode(conf.MountPath, address, cfg.RequiredFormat.String(), sinkOpts...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error configuring sink node: %w", op, err) | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return b, nil | 	return b, nil | ||||||
| @@ -163,11 +168,13 @@ func (b *Backend) Invalidate(_ context.Context) { | |||||||
| // newFormatterConfig creates the configuration required by a formatter node using | // newFormatterConfig creates the configuration required by a formatter node using | ||||||
| // the config map supplied to the factory. | // the config map supplied to the factory. | ||||||
| func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string]string) (audit.FormatterConfig, error) { | func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string]string) (audit.FormatterConfig, error) { | ||||||
| 	const op = "socket.newFormatterConfig" |  | ||||||
|  |  | ||||||
| 	var opts []audit.Option | 	var opts []audit.Option | ||||||
|  |  | ||||||
| 	if format, ok := config["format"]; ok { | 	if format, ok := config["format"]; ok { | ||||||
|  | 		if !audit.IsValidFormat(format) { | ||||||
|  | 			return audit.FormatterConfig{}, fmt.Errorf("unsupported 'format': %w", audit.ErrExternalOptions) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		opts = append(opts, audit.WithFormat(format)) | 		opts = append(opts, audit.WithFormat(format)) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -175,7 +182,7 @@ func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string | |||||||
| 	if hmacAccessorRaw, ok := config["hmac_accessor"]; ok { | 	if hmacAccessorRaw, ok := config["hmac_accessor"]; ok { | ||||||
| 		v, err := strconv.ParseBool(hmacAccessorRaw) | 		v, err := strconv.ParseBool(hmacAccessorRaw) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return audit.FormatterConfig{}, fmt.Errorf("%s: unable to parse 'hmac_accessor': %w", op, err) | 			return audit.FormatterConfig{}, fmt.Errorf("unable to parse 'hmac_accessor': %w", audit.ErrExternalOptions) | ||||||
| 		} | 		} | ||||||
| 		opts = append(opts, audit.WithHMACAccessor(v)) | 		opts = append(opts, audit.WithHMACAccessor(v)) | ||||||
| 	} | 	} | ||||||
| @@ -184,7 +191,7 @@ func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string | |||||||
| 	if raw, ok := config["log_raw"]; ok { | 	if raw, ok := config["log_raw"]; ok { | ||||||
| 		v, err := strconv.ParseBool(raw) | 		v, err := strconv.ParseBool(raw) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return audit.FormatterConfig{}, fmt.Errorf("%s: unable to parse 'log_raw': %w", op, err) | 			return audit.FormatterConfig{}, fmt.Errorf("unable to parse 'log_raw: %w", audit.ErrExternalOptions) | ||||||
| 		} | 		} | ||||||
| 		opts = append(opts, audit.WithRaw(v)) | 		opts = append(opts, audit.WithRaw(v)) | ||||||
| 	} | 	} | ||||||
| @@ -192,7 +199,7 @@ func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string | |||||||
| 	if elideListResponsesRaw, ok := config["elide_list_responses"]; ok { | 	if elideListResponsesRaw, ok := config["elide_list_responses"]; ok { | ||||||
| 		v, err := strconv.ParseBool(elideListResponsesRaw) | 		v, err := strconv.ParseBool(elideListResponsesRaw) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return audit.FormatterConfig{}, fmt.Errorf("%s: unable to parse 'elide_list_responses': %w", op, err) | 			return audit.FormatterConfig{}, fmt.Errorf("unable to parse 'elide_list_responses': %w", audit.ErrExternalOptions) | ||||||
| 		} | 		} | ||||||
| 		opts = append(opts, audit.WithElision(v)) | 		opts = append(opts, audit.WithElision(v)) | ||||||
| 	} | 	} | ||||||
| @@ -206,16 +213,14 @@ func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string | |||||||
|  |  | ||||||
| // configureFormatterNode is used to configure a formatter node and associated ID on the Backend. | // configureFormatterNode is used to configure a formatter node and associated ID on the Backend. | ||||||
| func (b *Backend) configureFormatterNode(name string, formatConfig audit.FormatterConfig, logger hclog.Logger) error { | func (b *Backend) configureFormatterNode(name string, formatConfig audit.FormatterConfig, logger hclog.Logger) error { | ||||||
| 	const op = "socket.(Backend).configureFormatterNode" |  | ||||||
|  |  | ||||||
| 	formatterNodeID, err := event.GenerateNodeID() | 	formatterNodeID, err := event.GenerateNodeID() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: error generating random NodeID for formatter node: %w", op, err) | 		return fmt.Errorf("error generating random NodeID for formatter node: %w: %w", audit.ErrInternal, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	formatterNode, err := audit.NewEntryFormatter(name, formatConfig, b, logger) | 	formatterNode, err := audit.NewEntryFormatter(name, formatConfig, b, logger) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: error creating formatter: %w", op, err) | 		return fmt.Errorf("error creating formatter: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	b.nodeIDList = append(b.nodeIDList, formatterNodeID) | 	b.nodeIDList = append(b.nodeIDList, formatterNodeID) | ||||||
| @@ -226,37 +231,35 @@ func (b *Backend) configureFormatterNode(name string, formatConfig audit.Formatt | |||||||
|  |  | ||||||
| // configureSinkNode is used to configure a sink node and associated ID on the Backend. | // configureSinkNode is used to configure a sink node and associated ID on the Backend. | ||||||
| func (b *Backend) configureSinkNode(name string, address string, format string, opts ...event.Option) error { | func (b *Backend) configureSinkNode(name string, address string, format string, opts ...event.Option) error { | ||||||
| 	const op = "socket.(Backend).configureSinkNode" |  | ||||||
|  |  | ||||||
| 	name = strings.TrimSpace(name) | 	name = strings.TrimSpace(name) | ||||||
| 	if name == "" { | 	if name == "" { | ||||||
| 		return fmt.Errorf("%s: name is required: %w", op, event.ErrInvalidParameter) | 		return fmt.Errorf("name is required: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	address = strings.TrimSpace(address) | 	address = strings.TrimSpace(address) | ||||||
| 	if address == "" { | 	if address == "" { | ||||||
| 		return fmt.Errorf("%s: address is required: %w", op, event.ErrInvalidParameter) | 		return fmt.Errorf("address is required: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	format = strings.TrimSpace(format) | 	format = strings.TrimSpace(format) | ||||||
| 	if format == "" { | 	if format == "" { | ||||||
| 		return fmt.Errorf("%s: format is required: %w", op, event.ErrInvalidParameter) | 		return fmt.Errorf("format is required: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	sinkNodeID, err := event.GenerateNodeID() | 	sinkNodeID, err := event.GenerateNodeID() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: error generating random NodeID for sink node: %w", op, err) | 		return fmt.Errorf("error generating random NodeID for sink node: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	n, err := event.NewSocketSink(address, format, opts...) | 	n, err := event.NewSocketSink(address, format, opts...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: error creating socket sink node: %w", op, err) | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Wrap the sink node with metrics middleware | 	// Wrap the sink node with metrics middleware | ||||||
| 	sinkMetricTimer, err := audit.NewSinkMetricTimer(name, n) | 	sinkMetricTimer, err := audit.NewSinkMetricTimer(name, n) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to add timing metrics to sink for path %q: %w", op, name, err) | 		return fmt.Errorf("unable to add timing metrics to sink for path %q: %w", name, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Decide what kind of labels we want and wrap the sink node inside a metrics counter. | 	// Decide what kind of labels we want and wrap the sink node inside a metrics counter. | ||||||
| @@ -270,7 +273,7 @@ func (b *Backend) configureSinkNode(name string, address string, format string, | |||||||
|  |  | ||||||
| 	sinkMetricCounter, err := event.NewMetricsCounter(name, sinkMetricTimer, metricLabeler) | 	sinkMetricCounter, err := event.NewMetricsCounter(name, sinkMetricTimer, metricLabeler) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to add counting metrics to sink for path %q: %w", op, name, err) | 		return fmt.Errorf("unable to add counting metrics to sink for path %q: %w", name, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	b.nodeIDList = append(b.nodeIDList, sinkNodeID) | 	b.nodeIDList = append(b.nodeIDList, sinkNodeID) | ||||||
| @@ -296,7 +299,7 @@ func (b *Backend) NodeIDs() []eventlogger.NodeID { | |||||||
|  |  | ||||||
| // EventType returns the event type for the backend. | // EventType returns the event type for the backend. | ||||||
| func (b *Backend) EventType() eventlogger.EventType { | func (b *Backend) EventType() eventlogger.EventType { | ||||||
| 	return eventlogger.EventType(event.AuditType.String()) | 	return event.AuditType.AsEventType() | ||||||
| } | } | ||||||
|  |  | ||||||
| // HasFiltering determines if the first node for the pipeline is an eventlogger.NodeTypeFilter. | // HasFiltering determines if the first node for the pipeline is an eventlogger.NodeTypeFilter. | ||||||
|   | |||||||
| @@ -65,7 +65,7 @@ func TestBackend_newFormatterConfig(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 			want:           audit.FormatterConfig{}, | 			want:           audit.FormatterConfig{}, | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "audit.NewFormatterConfig: error applying options: audit.(format).validate: 'squiggly' is not a valid format: invalid parameter", | 			expectedErrMsg: "unsupported 'format': invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"invalid-hmac-accessor": { | 		"invalid-hmac-accessor": { | ||||||
| 			config: map[string]string{ | 			config: map[string]string{ | ||||||
| @@ -74,7 +74,7 @@ func TestBackend_newFormatterConfig(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 			want:           audit.FormatterConfig{}, | 			want:           audit.FormatterConfig{}, | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "socket.newFormatterConfig: unable to parse 'hmac_accessor': strconv.ParseBool: parsing \"maybe\": invalid syntax", | 			expectedErrMsg: "unable to parse 'hmac_accessor': invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"invalid-log-raw": { | 		"invalid-log-raw": { | ||||||
| 			config: map[string]string{ | 			config: map[string]string{ | ||||||
| @@ -84,7 +84,7 @@ func TestBackend_newFormatterConfig(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 			want:           audit.FormatterConfig{}, | 			want:           audit.FormatterConfig{}, | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "socket.newFormatterConfig: unable to parse 'log_raw': strconv.ParseBool: parsing \"maybe\": invalid syntax", | 			expectedErrMsg: "unable to parse 'log_raw: invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"invalid-elide-bool": { | 		"invalid-elide-bool": { | ||||||
| 			config: map[string]string{ | 			config: map[string]string{ | ||||||
| @@ -95,7 +95,7 @@ func TestBackend_newFormatterConfig(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 			want:           audit.FormatterConfig{}, | 			want:           audit.FormatterConfig{}, | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "socket.newFormatterConfig: unable to parse 'elide_list_responses': strconv.ParseBool: parsing \"maybe\": invalid syntax", | 			expectedErrMsg: "unable to parse 'elide_list_responses': invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"prefix": { | 		"prefix": { | ||||||
| 			config: map[string]string{ | 			config: map[string]string{ | ||||||
| @@ -172,39 +172,39 @@ func TestBackend_configureSinkNode(t *testing.T) { | |||||||
| 			name:           "", | 			name:           "", | ||||||
| 			address:        "wss://foo", | 			address:        "wss://foo", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "socket.(Backend).configureSinkNode: name is required: invalid parameter", | 			expectedErrMsg: "name is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"name-whitespace": { | 		"name-whitespace": { | ||||||
| 			name:           "   ", | 			name:           "   ", | ||||||
| 			address:        "wss://foo", | 			address:        "wss://foo", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "socket.(Backend).configureSinkNode: name is required: invalid parameter", | 			expectedErrMsg: "name is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"address-empty": { | 		"address-empty": { | ||||||
| 			name:           "foo", | 			name:           "foo", | ||||||
| 			address:        "", | 			address:        "", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "socket.(Backend).configureSinkNode: address is required: invalid parameter", | 			expectedErrMsg: "address is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"address-whitespace": { | 		"address-whitespace": { | ||||||
| 			name:           "foo", | 			name:           "foo", | ||||||
| 			address:        "   ", | 			address:        "   ", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "socket.(Backend).configureSinkNode: address is required: invalid parameter", | 			expectedErrMsg: "address is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"format-empty": { | 		"format-empty": { | ||||||
| 			name:           "foo", | 			name:           "foo", | ||||||
| 			address:        "wss://foo", | 			address:        "wss://foo", | ||||||
| 			format:         "", | 			format:         "", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "socket.(Backend).configureSinkNode: format is required: invalid parameter", | 			expectedErrMsg: "format is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"format-whitespace": { | 		"format-whitespace": { | ||||||
| 			name:           "foo", | 			name:           "foo", | ||||||
| 			address:        "wss://foo", | 			address:        "wss://foo", | ||||||
| 			format:         "   ", | 			format:         "   ", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "socket.(Backend).configureSinkNode: format is required: invalid parameter", | 			expectedErrMsg: "format is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"happy": { | 		"happy": { | ||||||
| 			name:         "foo", | 			name:         "foo", | ||||||
| @@ -265,14 +265,14 @@ func TestBackend_Factory_Conf(t *testing.T) { | |||||||
| 				SaltConfig: nil, | 				SaltConfig: nil, | ||||||
| 			}, | 			}, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "socket.Factory: nil salt config", | 			expectedErrorMessage: "nil salt config: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"nil-salt-view": { | 		"nil-salt-view": { | ||||||
| 			backendConfig: &audit.BackendConfig{ | 			backendConfig: &audit.BackendConfig{ | ||||||
| 				SaltConfig: &salt.Config{}, | 				SaltConfig: &salt.Config{}, | ||||||
| 			}, | 			}, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "socket.Factory: nil salt view", | 			expectedErrorMessage: "nil salt view: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"nil-logger": { | 		"nil-logger": { | ||||||
| 			backendConfig: &audit.BackendConfig{ | 			backendConfig: &audit.BackendConfig{ | ||||||
| @@ -282,7 +282,7 @@ func TestBackend_Factory_Conf(t *testing.T) { | |||||||
| 				Logger:     nil, | 				Logger:     nil, | ||||||
| 			}, | 			}, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "socket.Factory: nil logger", | 			expectedErrorMessage: "nil logger: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"no-address": { | 		"no-address": { | ||||||
| 			backendConfig: &audit.BackendConfig{ | 			backendConfig: &audit.BackendConfig{ | ||||||
| @@ -293,7 +293,7 @@ func TestBackend_Factory_Conf(t *testing.T) { | |||||||
| 				Config:     map[string]string{}, | 				Config:     map[string]string{}, | ||||||
| 			}, | 			}, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "socket.Factory: address is required", | 			expectedErrorMessage: "address is required: invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"empty-address": { | 		"empty-address": { | ||||||
| 			backendConfig: &audit.BackendConfig{ | 			backendConfig: &audit.BackendConfig{ | ||||||
| @@ -306,7 +306,7 @@ func TestBackend_Factory_Conf(t *testing.T) { | |||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "socket.Factory: error configuring sink node: socket.(Backend).configureSinkNode: address is required: invalid parameter", | 			expectedErrorMessage: "address is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"whitespace-address": { | 		"whitespace-address": { | ||||||
| 			backendConfig: &audit.BackendConfig{ | 			backendConfig: &audit.BackendConfig{ | ||||||
| @@ -319,7 +319,7 @@ func TestBackend_Factory_Conf(t *testing.T) { | |||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "socket.Factory: error configuring sink node: socket.(Backend).configureSinkNode: address is required: invalid parameter", | 			expectedErrorMessage: "address is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"write-duration-valid": { | 		"write-duration-valid": { | ||||||
| 			backendConfig: &audit.BackendConfig{ | 			backendConfig: &audit.BackendConfig{ | ||||||
| @@ -346,7 +346,7 @@ func TestBackend_Factory_Conf(t *testing.T) { | |||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "socket.Factory: error configuring sink node: socket.(Backend).configureSinkNode: error creating socket sink node: event.NewSocketSink: error applying options: unable to parse max duration: time: invalid duration \"qwerty\"", | 			expectedErrorMessage: "unable to parse max duration: invalid parameter: time: invalid duration \"qwerty\"", | ||||||
| 		}, | 		}, | ||||||
| 		"non-fallback-device-with-filter": { | 		"non-fallback-device-with-filter": { | ||||||
| 			backendConfig: &audit.BackendConfig{ | 			backendConfig: &audit.BackendConfig{ | ||||||
| @@ -377,7 +377,7 @@ func TestBackend_Factory_Conf(t *testing.T) { | |||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "socket.Factory: cannot configure a fallback device with a filter: invalid parameter", | 			expectedErrorMessage: "cannot configure a fallback device with a filter: invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -35,18 +35,20 @@ type Backend struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| func Factory(_ context.Context, conf *audit.BackendConfig, headersConfig audit.HeaderFormatter) (audit.Backend, error) { | func Factory(_ context.Context, conf *audit.BackendConfig, headersConfig audit.HeaderFormatter) (audit.Backend, error) { | ||||||
| 	const op = "syslog.Factory" |  | ||||||
|  |  | ||||||
| 	if conf.SaltConfig == nil { | 	if conf.SaltConfig == nil { | ||||||
| 		return nil, fmt.Errorf("%s: nil salt config", op) | 		return nil, fmt.Errorf("nil salt config: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if conf.SaltView == nil { | 	if conf.SaltView == nil { | ||||||
| 		return nil, fmt.Errorf("%s: nil salt view", op) | 		return nil, fmt.Errorf("nil salt view: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if conf.Logger == nil || reflect.ValueOf(conf.Logger).IsNil() { | 	if conf.Logger == nil || reflect.ValueOf(conf.Logger).IsNil() { | ||||||
| 		return nil, fmt.Errorf("%s: nil logger", op) | 		return nil, fmt.Errorf("nil logger: %w", audit.ErrInvalidParameter) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if conf.MountPath == "" { | ||||||
|  | 		return nil, fmt.Errorf("mount path cannot be empty: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Get facility or default to AUTH | 	// Get facility or default to AUTH | ||||||
| @@ -61,19 +63,33 @@ func Factory(_ context.Context, conf *audit.BackendConfig, headersConfig audit.H | |||||||
| 		tag = "vault" | 		tag = "vault" | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	sinkOpts := []event.Option{ | ||||||
|  | 		event.WithFacility(facility), | ||||||
|  | 		event.WithTag(tag), | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	err := event.ValidateOptions(sinkOpts...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// The config options 'fallback' and 'filter' are mutually exclusive, a fallback | 	// The config options 'fallback' and 'filter' are mutually exclusive, a fallback | ||||||
| 	// device catches everything, so it cannot be allowed to filter. | 	// device catches everything, so it cannot be allowed to filter. | ||||||
| 	var fallback bool | 	var fallback bool | ||||||
| 	var err error |  | ||||||
| 	if fallbackRaw, ok := conf.Config["fallback"]; ok { | 	if fallbackRaw, ok := conf.Config["fallback"]; ok { | ||||||
| 		fallback, err = parseutil.ParseBool(fallbackRaw) | 		fallback, err = parseutil.ParseBool(fallbackRaw) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("%s: unable to parse 'fallback': %w", op, err) | 			return nil, fmt.Errorf("unable to parse 'fallback': %w", audit.ErrExternalOptions) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if _, ok := conf.Config["filter"]; ok && fallback { | 	if _, ok := conf.Config["filter"]; ok && fallback { | ||||||
| 		return nil, fmt.Errorf("%s: cannot configure a fallback device with a filter: %w", op, event.ErrInvalidParameter) | 		return nil, fmt.Errorf("cannot configure a fallback device with a filter: %w", audit.ErrExternalOptions) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	cfg, err := newFormatterConfig(headersConfig, conf.Config) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	b := &Backend{ | 	b := &Backend{ | ||||||
| @@ -87,27 +103,17 @@ func Factory(_ context.Context, conf *audit.BackendConfig, headersConfig audit.H | |||||||
|  |  | ||||||
| 	err = b.configureFilterNode(conf.Config["filter"]) | 	err = b.configureFilterNode(conf.Config["filter"]) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error configuring filter node: %w", op, err) | 		return nil, err | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	cfg, err := newFormatterConfig(headersConfig, conf.Config) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("%s: failed to create formatter config: %w", op, err) |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err = b.configureFormatterNode(conf.MountPath, cfg, conf.Logger) | 	err = b.configureFormatterNode(conf.MountPath, cfg, conf.Logger) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error configuring formatter node: %w", op, err) | 		return nil, err | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	sinkOpts := []event.Option{ |  | ||||||
| 		event.WithFacility(facility), |  | ||||||
| 		event.WithTag(tag), |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err = b.configureSinkNode(conf.MountPath, cfg.RequiredFormat.String(), sinkOpts...) | 	err = b.configureSinkNode(conf.MountPath, cfg.RequiredFormat.String(), sinkOpts...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error configuring sink node: %w", op, err) | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return b, nil | 	return b, nil | ||||||
| @@ -154,11 +160,13 @@ func (b *Backend) Invalidate(_ context.Context) { | |||||||
| // newFormatterConfig creates the configuration required by a formatter node using | // newFormatterConfig creates the configuration required by a formatter node using | ||||||
| // the config map supplied to the factory. | // the config map supplied to the factory. | ||||||
| func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string]string) (audit.FormatterConfig, error) { | func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string]string) (audit.FormatterConfig, error) { | ||||||
| 	const op = "syslog.newFormatterConfig" |  | ||||||
|  |  | ||||||
| 	var opts []audit.Option | 	var opts []audit.Option | ||||||
|  |  | ||||||
| 	if format, ok := config["format"]; ok { | 	if format, ok := config["format"]; ok { | ||||||
|  | 		if !audit.IsValidFormat(format) { | ||||||
|  | 			return audit.FormatterConfig{}, fmt.Errorf("unsupported 'format': %w", audit.ErrExternalOptions) | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		opts = append(opts, audit.WithFormat(format)) | 		opts = append(opts, audit.WithFormat(format)) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -166,7 +174,7 @@ func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string | |||||||
| 	if hmacAccessorRaw, ok := config["hmac_accessor"]; ok { | 	if hmacAccessorRaw, ok := config["hmac_accessor"]; ok { | ||||||
| 		v, err := strconv.ParseBool(hmacAccessorRaw) | 		v, err := strconv.ParseBool(hmacAccessorRaw) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return audit.FormatterConfig{}, fmt.Errorf("%s: unable to parse 'hmac_accessor': %w", op, err) | 			return audit.FormatterConfig{}, fmt.Errorf("unable to parse 'hmac_accessor': %w", audit.ErrExternalOptions) | ||||||
| 		} | 		} | ||||||
| 		opts = append(opts, audit.WithHMACAccessor(v)) | 		opts = append(opts, audit.WithHMACAccessor(v)) | ||||||
| 	} | 	} | ||||||
| @@ -175,7 +183,7 @@ func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string | |||||||
| 	if raw, ok := config["log_raw"]; ok { | 	if raw, ok := config["log_raw"]; ok { | ||||||
| 		v, err := strconv.ParseBool(raw) | 		v, err := strconv.ParseBool(raw) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return audit.FormatterConfig{}, fmt.Errorf("%s: unable to parse 'log_raw': %w", op, err) | 			return audit.FormatterConfig{}, fmt.Errorf("unable to parse 'log_raw: %w", audit.ErrExternalOptions) | ||||||
| 		} | 		} | ||||||
| 		opts = append(opts, audit.WithRaw(v)) | 		opts = append(opts, audit.WithRaw(v)) | ||||||
| 	} | 	} | ||||||
| @@ -183,7 +191,7 @@ func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string | |||||||
| 	if elideListResponsesRaw, ok := config["elide_list_responses"]; ok { | 	if elideListResponsesRaw, ok := config["elide_list_responses"]; ok { | ||||||
| 		v, err := strconv.ParseBool(elideListResponsesRaw) | 		v, err := strconv.ParseBool(elideListResponsesRaw) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return audit.FormatterConfig{}, fmt.Errorf("%s: unable to parse 'elide_list_responses': %w", op, err) | 			return audit.FormatterConfig{}, fmt.Errorf("unable to parse 'elide_list_responses': %w", audit.ErrExternalOptions) | ||||||
| 		} | 		} | ||||||
| 		opts = append(opts, audit.WithElision(v)) | 		opts = append(opts, audit.WithElision(v)) | ||||||
| 	} | 	} | ||||||
| @@ -197,16 +205,14 @@ func newFormatterConfig(headerFormatter audit.HeaderFormatter, config map[string | |||||||
|  |  | ||||||
| // configureFormatterNode is used to configure a formatter node and associated ID on the Backend. | // configureFormatterNode is used to configure a formatter node and associated ID on the Backend. | ||||||
| func (b *Backend) configureFormatterNode(name string, formatConfig audit.FormatterConfig, logger hclog.Logger) error { | func (b *Backend) configureFormatterNode(name string, formatConfig audit.FormatterConfig, logger hclog.Logger) error { | ||||||
| 	const op = "syslog.(Backend).configureFormatterNode" |  | ||||||
|  |  | ||||||
| 	formatterNodeID, err := event.GenerateNodeID() | 	formatterNodeID, err := event.GenerateNodeID() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: error generating random NodeID for formatter node: %w", op, err) | 		return fmt.Errorf("error generating random NodeID for formatter node: %w: %w", audit.ErrInternal, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	formatterNode, err := audit.NewEntryFormatter(name, formatConfig, b, logger) | 	formatterNode, err := audit.NewEntryFormatter(name, formatConfig, b, logger) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: error creating formatter: %w", op, err) | 		return fmt.Errorf("error creating formatter: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	b.nodeIDList = append(b.nodeIDList, formatterNodeID) | 	b.nodeIDList = append(b.nodeIDList, formatterNodeID) | ||||||
| @@ -217,32 +223,30 @@ func (b *Backend) configureFormatterNode(name string, formatConfig audit.Formatt | |||||||
|  |  | ||||||
| // configureSinkNode is used to configure a sink node and associated ID on the Backend. | // configureSinkNode is used to configure a sink node and associated ID on the Backend. | ||||||
| func (b *Backend) configureSinkNode(name string, format string, opts ...event.Option) error { | func (b *Backend) configureSinkNode(name string, format string, opts ...event.Option) error { | ||||||
| 	const op = "syslog.(Backend).configureSinkNode" |  | ||||||
|  |  | ||||||
| 	name = strings.TrimSpace(name) | 	name = strings.TrimSpace(name) | ||||||
| 	if name == "" { | 	if name == "" { | ||||||
| 		return fmt.Errorf("%s: name is required: %w", op, event.ErrInvalidParameter) | 		return fmt.Errorf("name is required: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	format = strings.TrimSpace(format) | 	format = strings.TrimSpace(format) | ||||||
| 	if format == "" { | 	if format == "" { | ||||||
| 		return fmt.Errorf("%s: format is required: %w", op, event.ErrInvalidParameter) | 		return fmt.Errorf("format is required: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	sinkNodeID, err := event.GenerateNodeID() | 	sinkNodeID, err := event.GenerateNodeID() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: error generating random NodeID for sink node: %w", op, err) | 		return fmt.Errorf("error generating random NodeID for sink node: %w: %w", audit.ErrInternal, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	n, err := event.NewSyslogSink(format, opts...) | 	n, err := event.NewSyslogSink(format, opts...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: error creating syslog sink node: %w", op, err) | 		return fmt.Errorf("error creating syslog sink node: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Wrap the sink node with metrics middleware | 	// Wrap the sink node with metrics middleware | ||||||
| 	sinkMetricTimer, err := audit.NewSinkMetricTimer(name, n) | 	sinkMetricTimer, err := audit.NewSinkMetricTimer(name, n) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to add timing metrics to sink for path %q: %w", op, name, err) | 		return fmt.Errorf("unable to add timing metrics to sink for path %q: %w", name, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Decide what kind of labels we want and wrap the sink node inside a metrics counter. | 	// Decide what kind of labels we want and wrap the sink node inside a metrics counter. | ||||||
| @@ -256,7 +260,7 @@ func (b *Backend) configureSinkNode(name string, format string, opts ...event.Op | |||||||
|  |  | ||||||
| 	sinkMetricCounter, err := event.NewMetricsCounter(name, sinkMetricTimer, metricLabeler) | 	sinkMetricCounter, err := event.NewMetricsCounter(name, sinkMetricTimer, metricLabeler) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to add counting metrics to sink for path %q: %w", op, name, err) | 		return fmt.Errorf("unable to add counting metrics to sink for path %q: %w", name, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	b.nodeIDList = append(b.nodeIDList, sinkNodeID) | 	b.nodeIDList = append(b.nodeIDList, sinkNodeID) | ||||||
| @@ -282,7 +286,7 @@ func (b *Backend) NodeIDs() []eventlogger.NodeID { | |||||||
|  |  | ||||||
| // EventType returns the event type for the backend. | // EventType returns the event type for the backend. | ||||||
| func (b *Backend) EventType() eventlogger.EventType { | func (b *Backend) EventType() eventlogger.EventType { | ||||||
| 	return eventlogger.EventType(event.AuditType.String()) | 	return event.AuditType.AsEventType() | ||||||
| } | } | ||||||
|  |  | ||||||
| // HasFiltering determines if the first node for the pipeline is an eventlogger.NodeTypeFilter. | // HasFiltering determines if the first node for the pipeline is an eventlogger.NodeTypeFilter. | ||||||
|   | |||||||
| @@ -65,7 +65,7 @@ func TestBackend_newFormatterConfig(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 			want:           audit.FormatterConfig{}, | 			want:           audit.FormatterConfig{}, | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "audit.NewFormatterConfig: error applying options: audit.(format).validate: 'squiggly' is not a valid format: invalid parameter", | 			expectedErrMsg: "unsupported 'format': invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"invalid-hmac-accessor": { | 		"invalid-hmac-accessor": { | ||||||
| 			config: map[string]string{ | 			config: map[string]string{ | ||||||
| @@ -74,7 +74,7 @@ func TestBackend_newFormatterConfig(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 			want:           audit.FormatterConfig{}, | 			want:           audit.FormatterConfig{}, | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "syslog.newFormatterConfig: unable to parse 'hmac_accessor': strconv.ParseBool: parsing \"maybe\": invalid syntax", | 			expectedErrMsg: "unable to parse 'hmac_accessor': invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"invalid-log-raw": { | 		"invalid-log-raw": { | ||||||
| 			config: map[string]string{ | 			config: map[string]string{ | ||||||
| @@ -84,7 +84,7 @@ func TestBackend_newFormatterConfig(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 			want:           audit.FormatterConfig{}, | 			want:           audit.FormatterConfig{}, | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "syslog.newFormatterConfig: unable to parse 'log_raw': strconv.ParseBool: parsing \"maybe\": invalid syntax", | 			expectedErrMsg: "unable to parse 'log_raw: invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"invalid-elide-bool": { | 		"invalid-elide-bool": { | ||||||
| 			config: map[string]string{ | 			config: map[string]string{ | ||||||
| @@ -95,7 +95,7 @@ func TestBackend_newFormatterConfig(t *testing.T) { | |||||||
| 			}, | 			}, | ||||||
| 			want:           audit.FormatterConfig{}, | 			want:           audit.FormatterConfig{}, | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "syslog.newFormatterConfig: unable to parse 'elide_list_responses': strconv.ParseBool: parsing \"maybe\": invalid syntax", | 			expectedErrMsg: "unable to parse 'elide_list_responses': invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 		"prefix": { | 		"prefix": { | ||||||
| 			config: map[string]string{ | 			config: map[string]string{ | ||||||
| @@ -170,24 +170,24 @@ func TestBackend_configureSinkNode(t *testing.T) { | |||||||
| 		"name-empty": { | 		"name-empty": { | ||||||
| 			name:           "", | 			name:           "", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "syslog.(Backend).configureSinkNode: name is required: invalid parameter", | 			expectedErrMsg: "name is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"name-whitespace": { | 		"name-whitespace": { | ||||||
| 			name:           "   ", | 			name:           "   ", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "syslog.(Backend).configureSinkNode: name is required: invalid parameter", | 			expectedErrMsg: "name is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"format-empty": { | 		"format-empty": { | ||||||
| 			name:           "foo", | 			name:           "foo", | ||||||
| 			format:         "", | 			format:         "", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "syslog.(Backend).configureSinkNode: format is required: invalid parameter", | 			expectedErrMsg: "format is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"format-whitespace": { | 		"format-whitespace": { | ||||||
| 			name:           "foo", | 			name:           "foo", | ||||||
| 			format:         "   ", | 			format:         "   ", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "syslog.(Backend).configureSinkNode: format is required: invalid parameter", | 			expectedErrMsg: "format is required: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"happy": { | 		"happy": { | ||||||
| 			name:         "foo", | 			name:         "foo", | ||||||
| @@ -247,14 +247,14 @@ func TestBackend_Factory_Conf(t *testing.T) { | |||||||
| 				SaltConfig: nil, | 				SaltConfig: nil, | ||||||
| 			}, | 			}, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "syslog.Factory: nil salt config", | 			expectedErrorMessage: "nil salt config: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"nil-salt-view": { | 		"nil-salt-view": { | ||||||
| 			backendConfig: &audit.BackendConfig{ | 			backendConfig: &audit.BackendConfig{ | ||||||
| 				SaltConfig: &salt.Config{}, | 				SaltConfig: &salt.Config{}, | ||||||
| 			}, | 			}, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "syslog.Factory: nil salt view", | 			expectedErrorMessage: "nil salt view: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"non-fallback-device-with-filter": { | 		"non-fallback-device-with-filter": { | ||||||
| 			backendConfig: &audit.BackendConfig{ | 			backendConfig: &audit.BackendConfig{ | ||||||
| @@ -281,7 +281,7 @@ func TestBackend_Factory_Conf(t *testing.T) { | |||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "syslog.Factory: cannot configure a fallback device with a filter: invalid parameter", | 			expectedErrorMessage: "cannot configure a fallback device with a filter: invalid configuration", | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -524,7 +524,7 @@ func (n *NoopAudit) RegisterNodesAndPipeline(broker *eventlogger.Broker, name st | |||||||
|  |  | ||||||
| 	pipeline := eventlogger.Pipeline{ | 	pipeline := eventlogger.Pipeline{ | ||||||
| 		PipelineID: eventlogger.PipelineID(name), | 		PipelineID: eventlogger.PipelineID(name), | ||||||
| 		EventType:  eventlogger.EventType(event.AuditType.String()), | 		EventType:  event.AuditType.AsEventType(), | ||||||
| 		NodeIDs:    n.nodeIDList, | 		NodeIDs:    n.nodeIDList, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -600,7 +600,7 @@ func (tl *TestLogger) StopLogging() { | |||||||
| } | } | ||||||
|  |  | ||||||
| func (n *NoopAudit) EventType() eventlogger.EventType { | func (n *NoopAudit) EventType() eventlogger.EventType { | ||||||
| 	return eventlogger.EventType(event.AuditType.String()) | 	return event.AuditType.AsEventType() | ||||||
| } | } | ||||||
|  |  | ||||||
| func (n *NoopAudit) HasFiltering() bool { | func (n *NoopAudit) HasFiltering() bool { | ||||||
|   | |||||||
| @@ -19,12 +19,11 @@ const ( | |||||||
|  |  | ||||||
| // Validate ensures that EventType is one of the set of allowed event types. | // Validate ensures that EventType is one of the set of allowed event types. | ||||||
| func (t EventType) Validate() error { | func (t EventType) Validate() error { | ||||||
| 	const op = "event.(EventType).Validate" |  | ||||||
| 	switch t { | 	switch t { | ||||||
| 	case AuditType: | 	case AuditType: | ||||||
| 		return nil | 		return nil | ||||||
| 	default: | 	default: | ||||||
| 		return fmt.Errorf("%s: '%s' is not a valid event type: %w", op, t, ErrInvalidParameter) | 		return fmt.Errorf("invalid event type %q: %w", t, ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -40,3 +39,8 @@ func GenerateNodeID() (eventlogger.NodeID, error) { | |||||||
| func (t EventType) String() string { | func (t EventType) String() string { | ||||||
| 	return string(t) | 	return string(t) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // AsEventType returns the EventType in a format for eventlogger. | ||||||
|  | func (t EventType) AsEventType() eventlogger.EventType { | ||||||
|  | 	return eventlogger.EventType(t.String()) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -23,12 +23,12 @@ func TestEventType_Validate(t *testing.T) { | |||||||
| 		"empty": { | 		"empty": { | ||||||
| 			Value:         "", | 			Value:         "", | ||||||
| 			IsValid:       false, | 			IsValid:       false, | ||||||
| 			ExpectedError: "event.(EventType).Validate: '' is not a valid event type: invalid parameter", | 			ExpectedError: "invalid event type \"\": invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"random": { | 		"random": { | ||||||
| 			Value:         "random", | 			Value:         "random", | ||||||
| 			IsValid:       false, | 			IsValid:       false, | ||||||
| 			ExpectedError: "event.(EventType).Validate: 'random' is not a valid event type: invalid parameter", | 			ExpectedError: "invalid event type \"random\": invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -31,19 +31,17 @@ type Labeler interface { | |||||||
|  |  | ||||||
| // NewMetricsCounter should be used to create the MetricsCounter. | // NewMetricsCounter should be used to create the MetricsCounter. | ||||||
| func NewMetricsCounter(name string, node eventlogger.Node, labeler Labeler) (*MetricsCounter, error) { | func NewMetricsCounter(name string, node eventlogger.Node, labeler Labeler) (*MetricsCounter, error) { | ||||||
| 	const op = "event.NewMetricsCounter" |  | ||||||
|  |  | ||||||
| 	name = strings.TrimSpace(name) | 	name = strings.TrimSpace(name) | ||||||
| 	if name == "" { | 	if name == "" { | ||||||
| 		return nil, fmt.Errorf("%s: name is required: %w", op, ErrInvalidParameter) | 		return nil, fmt.Errorf("name is required: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if node == nil || reflect.ValueOf(node).IsNil() { | 	if node == nil || reflect.ValueOf(node).IsNil() { | ||||||
| 		return nil, fmt.Errorf("%s: node is required: %w", op, ErrInvalidParameter) | 		return nil, fmt.Errorf("node is required: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if labeler == nil || reflect.ValueOf(labeler).IsNil() { | 	if labeler == nil || reflect.ValueOf(labeler).IsNil() { | ||||||
| 		return nil, fmt.Errorf("%s: labeler is required: %w", op, ErrInvalidParameter) | 		return nil, fmt.Errorf("labeler is required: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return &MetricsCounter{ | 	return &MetricsCounter{ | ||||||
|   | |||||||
| @@ -39,20 +39,20 @@ func TestNewMetricsCounter(t *testing.T) { | |||||||
| 			node:                 nil, | 			node:                 nil, | ||||||
| 			labeler:              nil, | 			labeler:              nil, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "event.NewMetricsCounter: name is required: invalid parameter", | 			expectedErrorMessage: "name is required: invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"no-node": { | 		"no-node": { | ||||||
| 			name:                 "foo", | 			name:                 "foo", | ||||||
| 			node:                 nil, | 			node:                 nil, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "event.NewMetricsCounter: node is required: invalid parameter", | 			expectedErrorMessage: "node is required: invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"no-labeler": { | 		"no-labeler": { | ||||||
| 			name:                 "foo", | 			name:                 "foo", | ||||||
| 			node:                 &testEventLoggerNode{}, | 			node:                 &testEventLoggerNode{}, | ||||||
| 			labeler:              nil, | 			labeler:              nil, | ||||||
| 			isErrorExpected:      true, | 			isErrorExpected:      true, | ||||||
| 			expectedErrorMessage: "event.NewMetricsCounter: labeler is required: invalid parameter", | 			expectedErrorMessage: "labeler is required: invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,7 +4,6 @@ | |||||||
| package event | package event | ||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"errors" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"os" | 	"os" | ||||||
| 	"strconv" | 	"strconv" | ||||||
| @@ -59,18 +58,24 @@ func getOpts(opt ...Option) (options, error) { | |||||||
| 	return opts, nil | 	return opts, nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // ValidateOptions can be used to validate options before they are required. | ||||||
|  | func ValidateOptions(opt ...Option) error { | ||||||
|  | 	_, err := getOpts(opt...) | ||||||
|  |  | ||||||
|  | 	return err | ||||||
|  | } | ||||||
|  |  | ||||||
| // NewID is a bit of a modified NewID has been done to stop a circular | // NewID is a bit of a modified NewID has been done to stop a circular | ||||||
| // dependency with the errors package that is caused by importing | // dependency with the errors package that is caused by importing | ||||||
| // boundary/internal/db | // boundary/internal/db | ||||||
| func NewID(prefix string) (string, error) { | func NewID(prefix string) (string, error) { | ||||||
| 	const op = "event.NewID" |  | ||||||
| 	if prefix == "" { | 	if prefix == "" { | ||||||
| 		return "", fmt.Errorf("%s: missing prefix: %w", op, ErrInvalidParameter) | 		return "", fmt.Errorf("missing prefix: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	id, err := uuid.GenerateUUID() | 	id, err := uuid.GenerateUUID() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return "", fmt.Errorf("%s: unable to generate ID: %w", op, err) | 		return "", fmt.Errorf("unable to generate ID: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return fmt.Sprintf("%s_%s", prefix, id), nil | 	return fmt.Sprintf("%s_%s", prefix, id), nil | ||||||
| @@ -84,7 +89,7 @@ func WithID(id string) Option { | |||||||
| 		id := strings.TrimSpace(id) | 		id := strings.TrimSpace(id) | ||||||
| 		switch { | 		switch { | ||||||
| 		case id == "": | 		case id == "": | ||||||
| 			err = errors.New("id cannot be empty") | 			err = fmt.Errorf("id cannot be empty: %w", ErrInvalidParameter) | ||||||
| 		default: | 		default: | ||||||
| 			o.withID = id | 			o.withID = id | ||||||
| 		} | 		} | ||||||
| @@ -100,7 +105,7 @@ func WithNow(now time.Time) Option { | |||||||
|  |  | ||||||
| 		switch { | 		switch { | ||||||
| 		case now.IsZero(): | 		case now.IsZero(): | ||||||
| 			err = errors.New("cannot specify 'now' to be the zero time instant") | 			err = fmt.Errorf("cannot specify 'now' to be the zero time instant: %w", ErrInvalidParameter) | ||||||
| 		default: | 		default: | ||||||
| 			o.withNow = now | 			o.withNow = now | ||||||
| 		} | 		} | ||||||
| @@ -159,7 +164,7 @@ func WithMaxDuration(duration string) Option { | |||||||
|  |  | ||||||
| 		parsed, err := parseutil.ParseDurationSecond(duration) | 		parsed, err := parseutil.ParseDurationSecond(duration) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return fmt.Errorf("unable to parse max duration: %w", err) | 			return fmt.Errorf("unable to parse max duration: %w: %w", ErrInvalidParameter, err) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		o.withMaxDuration = parsed | 		o.withMaxDuration = parsed | ||||||
| @@ -187,7 +192,7 @@ func WithFileMode(mode string) Option { | |||||||
|  |  | ||||||
| 		switch { | 		switch { | ||||||
| 		case err != nil: | 		case err != nil: | ||||||
| 			return fmt.Errorf("unable to parse file mode: %w", err) | 			return fmt.Errorf("unable to parse file mode: %w: %w", ErrInvalidParameter, err) | ||||||
| 		default: | 		default: | ||||||
| 			m := os.FileMode(raw) | 			m := os.FileMode(raw) | ||||||
| 			o.withFileMode = &m | 			o.withFileMode = &m | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ func TestOptions_WithNow(t *testing.T) { | |||||||
| 		"default-time": { | 		"default-time": { | ||||||
| 			Value:                time.Time{}, | 			Value:                time.Time{}, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "cannot specify 'now' to be the zero time instant", | 			ExpectedErrorMessage: "cannot specify 'now' to be the zero time instant: invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"valid-time": { | 		"valid-time": { | ||||||
| 			Value:           time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local), | 			Value:           time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local), | ||||||
| @@ -63,12 +63,12 @@ func TestOptions_WithID(t *testing.T) { | |||||||
| 		"empty": { | 		"empty": { | ||||||
| 			Value:                "", | 			Value:                "", | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "id cannot be empty", | 			ExpectedErrorMessage: "id cannot be empty: invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"whitespace": { | 		"whitespace": { | ||||||
| 			Value:                "     ", | 			Value:                "     ", | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "id cannot be empty", | 			ExpectedErrorMessage: "id cannot be empty: invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"valid": { | 		"valid": { | ||||||
| 			Value:           "test", | 			Value:           "test", | ||||||
| @@ -152,7 +152,7 @@ func TestOptions_Opts(t *testing.T) { | |||||||
| 				WithNow(time.Time{}), | 				WithNow(time.Time{}), | ||||||
| 			}, | 			}, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "cannot specify 'now' to be the zero time instant", | 			ExpectedErrorMessage: "cannot specify 'now' to be the zero time instant: invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"with-multiple-valid-options": { | 		"with-multiple-valid-options": { | ||||||
| 			opts: []Option{ | 			opts: []Option{ | ||||||
| @@ -324,12 +324,12 @@ func TestOptions_WithMaxDuration(t *testing.T) { | |||||||
| 		"bad-value": { | 		"bad-value": { | ||||||
| 			Value:                "juan", | 			Value:                "juan", | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "unable to parse max duration: time: invalid duration \"juan\"", | 			ExpectedErrorMessage: "unable to parse max duration: invalid parameter: time: invalid duration \"juan\"", | ||||||
| 		}, | 		}, | ||||||
| 		"bad-spacey-value": { | 		"bad-spacey-value": { | ||||||
| 			Value:                "   juan   ", | 			Value:                "   juan   ", | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "unable to parse max duration: time: invalid duration \"juan\"", | 			ExpectedErrorMessage: "unable to parse max duration: invalid parameter: time: invalid duration \"juan\"", | ||||||
| 		}, | 		}, | ||||||
| 		"duration-2s": { | 		"duration-2s": { | ||||||
| 			Value:         "2s", | 			Value:         "2s", | ||||||
| @@ -383,7 +383,7 @@ func TestOptions_WithFileMode(t *testing.T) { | |||||||
| 		"nonsense": { | 		"nonsense": { | ||||||
| 			Value:                "juan", | 			Value:                "juan", | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "unable to parse file mode: strconv.ParseUint: parsing \"juan\": invalid syntax", | 			ExpectedErrorMessage: "unable to parse file mode: invalid parameter: strconv.ParseUint: parsing \"juan\": invalid syntax", | ||||||
| 		}, | 		}, | ||||||
| 		"zero": { | 		"zero": { | ||||||
| 			Value:           "0000", | 			Value:           "0000", | ||||||
|   | |||||||
| @@ -36,17 +36,15 @@ type FileSink struct { | |||||||
| // NewFileSink should be used to create a new FileSink. | // NewFileSink should be used to create a new FileSink. | ||||||
| // Accepted options: WithFileMode. | // Accepted options: WithFileMode. | ||||||
| func NewFileSink(path string, format string, opt ...Option) (*FileSink, error) { | func NewFileSink(path string, format string, opt ...Option) (*FileSink, error) { | ||||||
| 	const op = "event.NewFileSink" |  | ||||||
|  |  | ||||||
| 	// Parse and check path | 	// Parse and check path | ||||||
| 	p := strings.TrimSpace(path) | 	p := strings.TrimSpace(path) | ||||||
| 	if p == "" { | 	if p == "" { | ||||||
| 		return nil, fmt.Errorf("%s: path is required", op) | 		return nil, fmt.Errorf("path is required: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	opts, err := getOpts(opt...) | 	opts, err := getOpts(opt...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error applying options: %w", op, err) | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	mode := os.FileMode(defaultFileMode) | 	mode := os.FileMode(defaultFileMode) | ||||||
| @@ -58,7 +56,7 @@ func NewFileSink(path string, format string, opt ...Option) (*FileSink, error) { | |||||||
| 	case *opts.withFileMode == 0: // Maintain the existing file's mode when set to "0000". | 	case *opts.withFileMode == 0: // Maintain the existing file's mode when set to "0000". | ||||||
| 		fileInfo, err := os.Stat(path) | 		fileInfo, err := os.Stat(path) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("%s: unable to determine existing file mode: %w", op, err) | 			return nil, fmt.Errorf("unable to determine existing file mode: %w", err) | ||||||
| 		} | 		} | ||||||
| 		mode = fileInfo.Mode() | 		mode = fileInfo.Mode() | ||||||
| 	default: | 	default: | ||||||
| @@ -77,7 +75,7 @@ func NewFileSink(path string, format string, opt ...Option) (*FileSink, error) { | |||||||
| 	// otherwise it will be too late to catch later without problems | 	// otherwise it will be too late to catch later without problems | ||||||
| 	// (ref: https://github.com/hashicorp/vault/issues/550) | 	// (ref: https://github.com/hashicorp/vault/issues/550) | ||||||
| 	if err := sink.open(); err != nil { | 	if err := sink.open(); err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: sanity check failed; unable to open %q for writing: %w", op, path, err) | 		return nil, fmt.Errorf("sanity check failed; unable to open %q for writing: %w", sink.path, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return sink, nil | 	return sink, nil | ||||||
| @@ -85,8 +83,6 @@ func NewFileSink(path string, format string, opt ...Option) (*FileSink, error) { | |||||||
|  |  | ||||||
| // Process handles writing the event to the file sink. | // Process handles writing the event to the file sink. | ||||||
| func (s *FileSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) { | func (s *FileSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) { | ||||||
| 	const op = "event.(FileSink).Process" |  | ||||||
|  |  | ||||||
| 	select { | 	select { | ||||||
| 	case <-ctx.Done(): | 	case <-ctx.Done(): | ||||||
| 		return nil, ctx.Err() | 		return nil, ctx.Err() | ||||||
| @@ -94,7 +90,7 @@ func (s *FileSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlog | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if e == nil { | 	if e == nil { | ||||||
| 		return nil, fmt.Errorf("%s: event is nil: %w", op, ErrInvalidParameter) | 		return nil, fmt.Errorf("event is nil: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// '/dev/null' path means we just do nothing and pretend we're done. | 	// '/dev/null' path means we just do nothing and pretend we're done. | ||||||
| @@ -104,12 +100,12 @@ func (s *FileSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlog | |||||||
|  |  | ||||||
| 	formatted, found := e.Format(s.requiredFormat) | 	formatted, found := e.Format(s.requiredFormat) | ||||||
| 	if !found { | 	if !found { | ||||||
| 		return nil, fmt.Errorf("%s: unable to retrieve event formatted as %q", op, s.requiredFormat) | 		return nil, fmt.Errorf("unable to retrieve event formatted as %q: %w", s.requiredFormat, ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err := s.log(formatted) | 	err := s.log(formatted) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error writing file for sink: %w", op, err) | 		return nil, fmt.Errorf("error writing file for sink %q: %w", s.path, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// return nil for the event to indicate the pipeline is complete. | 	// return nil for the event to indicate the pipeline is complete. | ||||||
| @@ -118,8 +114,6 @@ func (s *FileSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlog | |||||||
|  |  | ||||||
| // Reopen handles closing and reopening the file. | // Reopen handles closing and reopening the file. | ||||||
| func (s *FileSink) Reopen() error { | func (s *FileSink) Reopen() error { | ||||||
| 	const op = "event.(FileSink).Reopen" |  | ||||||
|  |  | ||||||
| 	// '/dev/null' path means we just do nothing and pretend we're done. | 	// '/dev/null' path means we just do nothing and pretend we're done. | ||||||
| 	if s.path == devnull { | 	if s.path == devnull { | ||||||
| 		return nil | 		return nil | ||||||
| @@ -136,7 +130,7 @@ func (s *FileSink) Reopen() error { | |||||||
| 	// Set to nil here so that even if we error out, on the next access open() will be tried. | 	// Set to nil here so that even if we error out, on the next access open() will be tried. | ||||||
| 	s.file = nil | 	s.file = nil | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to close file for re-opening on sink: %w", op, err) | 		return fmt.Errorf("unable to close file for re-opening on sink %q: %w", s.path, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return s.open() | 	return s.open() | ||||||
| @@ -152,20 +146,18 @@ func (s *FileSink) Type() eventlogger.NodeType { | |||||||
| // It doesn't have any locking and relies on calling functions of FileSink to | // It doesn't have any locking and relies on calling functions of FileSink to | ||||||
| // handle this (e.g. log and Reopen methods). | // handle this (e.g. log and Reopen methods). | ||||||
| func (s *FileSink) open() error { | func (s *FileSink) open() error { | ||||||
| 	const op = "event.(FileSink).open" |  | ||||||
|  |  | ||||||
| 	if s.file != nil { | 	if s.file != nil { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if err := os.MkdirAll(filepath.Dir(s.path), s.fileMode); err != nil { | 	if err := os.MkdirAll(filepath.Dir(s.path), s.fileMode); err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to create file %q: %w", op, s.path, err) | 		return fmt.Errorf("unable to create file %q: %w", s.path, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	var err error | 	var err error | ||||||
| 	s.file, err = os.OpenFile(s.path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, s.fileMode) | 	s.file, err = os.OpenFile(s.path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, s.fileMode) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to open file for sink: %w", op, err) | 		return fmt.Errorf("unable to open file for sink %q: %w", s.path, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Change the file mode in case the log file already existed. | 	// Change the file mode in case the log file already existed. | ||||||
| @@ -176,7 +168,7 @@ func (s *FileSink) open() error { | |||||||
| 		if s.fileMode != 0 { | 		if s.fileMode != 0 { | ||||||
| 			err = os.Chmod(s.path, s.fileMode) | 			err = os.Chmod(s.path, s.fileMode) | ||||||
| 			if err != nil { | 			if err != nil { | ||||||
| 				return fmt.Errorf("%s: unable to change file %q permissions '%v' for sink: %w", op, s.path, s.fileMode, err) | 				return fmt.Errorf("unable to change file permissions '%v' for sink %q: %w", s.fileMode, s.path, err) | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| @@ -187,15 +179,13 @@ func (s *FileSink) open() error { | |||||||
| // log writes the buffer to the file. | // log writes the buffer to the file. | ||||||
| // It acquires a lock on the file to do this. | // It acquires a lock on the file to do this. | ||||||
| func (s *FileSink) log(data []byte) error { | func (s *FileSink) log(data []byte) error { | ||||||
| 	const op = "event.(FileSink).log" |  | ||||||
|  |  | ||||||
| 	s.fileLock.Lock() | 	s.fileLock.Lock() | ||||||
| 	defer s.fileLock.Unlock() | 	defer s.fileLock.Unlock() | ||||||
|  |  | ||||||
| 	reader := bytes.NewReader(data) | 	reader := bytes.NewReader(data) | ||||||
|  |  | ||||||
| 	if err := s.open(); err != nil { | 	if err := s.open(); err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to open file for sink: %w", op, err) | 		return fmt.Errorf("unable to open file for sink %q: %w", s.path, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if _, err := reader.WriteTo(s.file); err == nil { | 	if _, err := reader.WriteTo(s.file); err == nil { | ||||||
| @@ -205,23 +195,23 @@ func (s *FileSink) log(data []byte) error { | |||||||
| 	// Otherwise, opportunistically try to re-open the FD, once per call (1 retry attempt). | 	// Otherwise, opportunistically try to re-open the FD, once per call (1 retry attempt). | ||||||
| 	err := s.file.Close() | 	err := s.file.Close() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to close file for sink: %w", op, err) | 		return fmt.Errorf("unable to close file for sink %q: %w", s.path, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	s.file = nil | 	s.file = nil | ||||||
|  |  | ||||||
| 	if err := s.open(); err != nil { | 	if err := s.open(); err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to re-open file for sink: %w", op, err) | 		return fmt.Errorf("unable to re-open file for sink %q: %w", s.path, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	_, err = reader.Seek(0, io.SeekStart) | 	_, err = reader.Seek(0, io.SeekStart) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to seek to start of file for sink: %w", op, err) | 		return fmt.Errorf("unable to seek to start of file for sink %q: %w", s.path, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	_, err = reader.WriteTo(s.file) | 	_, err = reader.WriteTo(s.file) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to re-write to file for sink: %w", op, err) | 		return fmt.Errorf("unable to re-write to file for sink %q: %w", s.path, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
|   | |||||||
| @@ -40,14 +40,14 @@ func TestNewFileSink(t *testing.T) { | |||||||
| 		"default-values": { | 		"default-values": { | ||||||
| 			ShouldUseAbsolutePath: true, | 			ShouldUseAbsolutePath: true, | ||||||
| 			IsErrorExpected:       true, | 			IsErrorExpected:       true, | ||||||
| 			ExpectedErrorMessage:  "event.NewFileSink: path is required", | 			ExpectedErrorMessage:  "path is required: invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"spacey-path": { | 		"spacey-path": { | ||||||
| 			ShouldUseAbsolutePath: true, | 			ShouldUseAbsolutePath: true, | ||||||
| 			Path:                  "     ", | 			Path:                  "     ", | ||||||
| 			Format:                "json", | 			Format:                "json", | ||||||
| 			IsErrorExpected:       true, | 			IsErrorExpected:       true, | ||||||
| 			ExpectedErrorMessage:  "event.NewFileSink: path is required", | 			ExpectedErrorMessage:  "path is required: invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"valid-path-and-format": { | 		"valid-path-and-format": { | ||||||
| 			Path:             "vault.log", | 			Path:             "vault.log", | ||||||
| @@ -226,7 +226,7 @@ func TestFileSink_Process(t *testing.T) { | |||||||
| 			Data:                 "foo", | 			Data:                 "foo", | ||||||
| 			ShouldIgnoreFormat:   true, | 			ShouldIgnoreFormat:   true, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "event.(FileSink).Process: unable to retrieve event formatted as \"json\"", | 			ExpectedErrorMessage: "unable to retrieve event formatted as \"json\": invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"nil": { | 		"nil": { | ||||||
| 			Path:                 "foo.log", | 			Path:                 "foo.log", | ||||||
| @@ -234,7 +234,7 @@ func TestFileSink_Process(t *testing.T) { | |||||||
| 			Data:                 "foo", | 			Data:                 "foo", | ||||||
| 			ShouldUseNilEvent:    true, | 			ShouldUseNilEvent:    true, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "event.(FileSink).Process: event is nil: invalid parameter", | 			ExpectedErrorMessage: "event is nil: invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -30,21 +30,19 @@ type SocketSink struct { | |||||||
| // NewSocketSink should be used to create a new SocketSink. | // NewSocketSink should be used to create a new SocketSink. | ||||||
| // Accepted options: WithMaxDuration and WithSocketType. | // Accepted options: WithMaxDuration and WithSocketType. | ||||||
| func NewSocketSink(address string, format string, opt ...Option) (*SocketSink, error) { | func NewSocketSink(address string, format string, opt ...Option) (*SocketSink, error) { | ||||||
| 	const op = "event.NewSocketSink" |  | ||||||
|  |  | ||||||
| 	address = strings.TrimSpace(address) | 	address = strings.TrimSpace(address) | ||||||
| 	if address == "" { | 	if address == "" { | ||||||
| 		return nil, fmt.Errorf("%s: address is required: %w", op, ErrInvalidParameter) | 		return nil, fmt.Errorf("address is required: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	format = strings.TrimSpace(format) | 	format = strings.TrimSpace(format) | ||||||
| 	if format == "" { | 	if format == "" { | ||||||
| 		return nil, fmt.Errorf("%s: format is required: %w", op, ErrInvalidParameter) | 		return nil, fmt.Errorf("format is required: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	opts, err := getOpts(opt...) | 	opts, err := getOpts(opt...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error applying options: %w", op, err) | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	sink := &SocketSink{ | 	sink := &SocketSink{ | ||||||
| @@ -61,8 +59,6 @@ func NewSocketSink(address string, format string, opt ...Option) (*SocketSink, e | |||||||
|  |  | ||||||
| // Process handles writing the event to the socket. | // Process handles writing the event to the socket. | ||||||
| func (s *SocketSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) { | func (s *SocketSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) { | ||||||
| 	const op = "event.(SocketSink).Process" |  | ||||||
|  |  | ||||||
| 	select { | 	select { | ||||||
| 	case <-ctx.Done(): | 	case <-ctx.Done(): | ||||||
| 		return nil, ctx.Err() | 		return nil, ctx.Err() | ||||||
| @@ -73,12 +69,12 @@ func (s *SocketSink) Process(ctx context.Context, e *eventlogger.Event) (*eventl | |||||||
| 	defer s.socketLock.Unlock() | 	defer s.socketLock.Unlock() | ||||||
|  |  | ||||||
| 	if e == nil { | 	if e == nil { | ||||||
| 		return nil, fmt.Errorf("%s: event is nil: %w", op, ErrInvalidParameter) | 		return nil, fmt.Errorf("event is nil: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	formatted, found := e.Format(s.requiredFormat) | 	formatted, found := e.Format(s.requiredFormat) | ||||||
| 	if !found { | 	if !found { | ||||||
| 		return nil, fmt.Errorf("%s: unable to retrieve event formatted as %q", op, s.requiredFormat) | 		return nil, fmt.Errorf("unable to retrieve event formatted as %q: %w", s.requiredFormat, ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Try writing and return early if successful. | 	// Try writing and return early if successful. | ||||||
| @@ -99,7 +95,7 @@ func (s *SocketSink) Process(ctx context.Context, e *eventlogger.Event) (*eventl | |||||||
|  |  | ||||||
| 	// Format the error nicely if we need to return one. | 	// Format the error nicely if we need to return one. | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		err = fmt.Errorf("%s: error writing to socket: %w", op, err) | 		err = fmt.Errorf("error writing to socket %q: %w", s.address, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// return nil for the event to indicate the pipeline is complete. | 	// return nil for the event to indicate the pipeline is complete. | ||||||
| @@ -108,14 +104,12 @@ func (s *SocketSink) Process(ctx context.Context, e *eventlogger.Event) (*eventl | |||||||
|  |  | ||||||
| // Reopen handles reopening the connection for the socket sink. | // Reopen handles reopening the connection for the socket sink. | ||||||
| func (s *SocketSink) Reopen() error { | func (s *SocketSink) Reopen() error { | ||||||
| 	const op = "event.(SocketSink).Reopen" |  | ||||||
|  |  | ||||||
| 	s.socketLock.Lock() | 	s.socketLock.Lock() | ||||||
| 	defer s.socketLock.Unlock() | 	defer s.socketLock.Unlock() | ||||||
|  |  | ||||||
| 	err := s.reconnect(nil) | 	err := s.reconnect(nil) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: error reconnecting: %w", op, err) | 		return fmt.Errorf("error reconnecting %q: %w", s.address, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| @@ -128,8 +122,6 @@ func (_ *SocketSink) Type() eventlogger.NodeType { | |||||||
|  |  | ||||||
| // connect attempts to establish a connection using the socketType and address. | // connect attempts to establish a connection using the socketType and address. | ||||||
| func (s *SocketSink) connect(ctx context.Context) error { | func (s *SocketSink) connect(ctx context.Context) error { | ||||||
| 	const op = "event.(SocketSink).connect" |  | ||||||
|  |  | ||||||
| 	// If we're already connected, we should have disconnected first. | 	// If we're already connected, we should have disconnected first. | ||||||
| 	if s.connection != nil { | 	if s.connection != nil { | ||||||
| 		return nil | 		return nil | ||||||
| @@ -141,7 +133,7 @@ func (s *SocketSink) connect(ctx context.Context) error { | |||||||
| 	dialer := net.Dialer{} | 	dialer := net.Dialer{} | ||||||
| 	conn, err := dialer.DialContext(timeoutContext, s.socketType, s.address) | 	conn, err := dialer.DialContext(timeoutContext, s.socketType, s.address) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: error connecting to %q address %q: %w", op, s.socketType, s.address, err) | 		return fmt.Errorf("error connecting to %q address %q: %w", s.socketType, s.address, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	s.connection = conn | 	s.connection = conn | ||||||
| @@ -151,8 +143,6 @@ func (s *SocketSink) connect(ctx context.Context) error { | |||||||
|  |  | ||||||
| // disconnect attempts to close and clear an existing connection. | // disconnect attempts to close and clear an existing connection. | ||||||
| func (s *SocketSink) disconnect() error { | func (s *SocketSink) disconnect() error { | ||||||
| 	const op = "event.(SocketSink).disconnect" |  | ||||||
|  |  | ||||||
| 	// If we're already disconnected, we can return early. | 	// If we're already disconnected, we can return early. | ||||||
| 	if s.connection == nil { | 	if s.connection == nil { | ||||||
| 		return nil | 		return nil | ||||||
| @@ -160,7 +150,7 @@ func (s *SocketSink) disconnect() error { | |||||||
|  |  | ||||||
| 	err := s.connection.Close() | 	err := s.connection.Close() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: error closing connection: %w", op, err) | 		return fmt.Errorf("error closing connection to %q address %q: %w", s.socketType, s.address, err) | ||||||
| 	} | 	} | ||||||
| 	s.connection = nil | 	s.connection = nil | ||||||
|  |  | ||||||
| @@ -169,16 +159,14 @@ func (s *SocketSink) disconnect() error { | |||||||
|  |  | ||||||
| // reconnect attempts to disconnect and then connect to the configured socketType and address. | // reconnect attempts to disconnect and then connect to the configured socketType and address. | ||||||
| func (s *SocketSink) reconnect(ctx context.Context) error { | func (s *SocketSink) reconnect(ctx context.Context) error { | ||||||
| 	const op = "event.(SocketSink).reconnect" |  | ||||||
|  |  | ||||||
| 	err := s.disconnect() | 	err := s.disconnect() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: error disconnecting: %w", op, err) | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err = s.connect(ctx) | 	err = s.connect(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: error connecting: %w", op, err) | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| @@ -186,22 +174,20 @@ func (s *SocketSink) reconnect(ctx context.Context) error { | |||||||
|  |  | ||||||
| // write attempts to write the specified data using the established connection. | // write attempts to write the specified data using the established connection. | ||||||
| func (s *SocketSink) write(ctx context.Context, data []byte) error { | func (s *SocketSink) write(ctx context.Context, data []byte) error { | ||||||
| 	const op = "event.(SocketSink).write" |  | ||||||
|  |  | ||||||
| 	// Ensure we're connected. | 	// Ensure we're connected. | ||||||
| 	err := s.connect(ctx) | 	err := s.connect(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: connection error: %w", op, err) | 		return err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	err = s.connection.SetWriteDeadline(time.Now().Add(s.maxDuration)) | 	err = s.connection.SetWriteDeadline(time.Now().Add(s.maxDuration)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to set write deadline: %w", op, err) | 		return fmt.Errorf("unable to set write deadline: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	_, err = s.connection.Write(data) | 	_, err = s.connection.Write(data) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to write to socket: %w", op, err) | 		return fmt.Errorf("unable to write to socket: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
|   | |||||||
| @@ -26,31 +26,31 @@ func TestNewSocketSink(t *testing.T) { | |||||||
| 		"address-empty": { | 		"address-empty": { | ||||||
| 			address:        "", | 			address:        "", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "event.NewSocketSink: address is required: invalid parameter", | 			expectedErrMsg: "address is required: invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"address-whitespace": { | 		"address-whitespace": { | ||||||
| 			address:        "    ", | 			address:        "    ", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "event.NewSocketSink: address is required: invalid parameter", | 			expectedErrMsg: "address is required: invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"format-empty": { | 		"format-empty": { | ||||||
| 			address:        "addr", | 			address:        "addr", | ||||||
| 			format:         "", | 			format:         "", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "event.NewSocketSink: format is required: invalid parameter", | 			expectedErrMsg: "format is required: invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"format-whitespace": { | 		"format-whitespace": { | ||||||
| 			address:        "addr", | 			address:        "addr", | ||||||
| 			format:         "   ", | 			format:         "   ", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "event.NewSocketSink: format is required: invalid parameter", | 			expectedErrMsg: "format is required: invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"bad-max-duration": { | 		"bad-max-duration": { | ||||||
| 			address:        "addr", | 			address:        "addr", | ||||||
| 			format:         "json", | 			format:         "json", | ||||||
| 			opts:           []Option{WithMaxDuration("bar")}, | 			opts:           []Option{WithMaxDuration("bar")}, | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "event.NewSocketSink: error applying options: unable to parse max duration: time: invalid duration \"bar\"", | 			expectedErrMsg: "unable to parse max duration: invalid parameter: time: invalid duration \"bar\"", | ||||||
| 		}, | 		}, | ||||||
| 		"happy": { | 		"happy": { | ||||||
| 			address: "wss://foo", | 			address: "wss://foo", | ||||||
|   | |||||||
| @@ -23,11 +23,9 @@ type StdoutSink struct { | |||||||
| // NewStdoutSinkNode creates a new StdoutSink that will persist the events | // NewStdoutSinkNode creates a new StdoutSink that will persist the events | ||||||
| // it processes using the specified expected format. | // it processes using the specified expected format. | ||||||
| func NewStdoutSinkNode(format string) (*StdoutSink, error) { | func NewStdoutSinkNode(format string) (*StdoutSink, error) { | ||||||
| 	const op = "event.NewStdoutSinkNode" |  | ||||||
|  |  | ||||||
| 	format = strings.TrimSpace(format) | 	format = strings.TrimSpace(format) | ||||||
| 	if format == "" { | 	if format == "" { | ||||||
| 		return nil, fmt.Errorf("%s: format is required: %w", op, ErrInvalidParameter) | 		return nil, fmt.Errorf("format is required: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return &StdoutSink{ | 	return &StdoutSink{ | ||||||
| @@ -37,8 +35,6 @@ func NewStdoutSinkNode(format string) (*StdoutSink, error) { | |||||||
|  |  | ||||||
| // Process persists the provided eventlogger.Event to the standard output stream. | // Process persists the provided eventlogger.Event to the standard output stream. | ||||||
| func (s *StdoutSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) { | func (s *StdoutSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) { | ||||||
| 	const op = "event.(StdoutSink).Process" |  | ||||||
|  |  | ||||||
| 	select { | 	select { | ||||||
| 	case <-ctx.Done(): | 	case <-ctx.Done(): | ||||||
| 		return nil, ctx.Err() | 		return nil, ctx.Err() | ||||||
| @@ -46,17 +42,17 @@ func (s *StdoutSink) Process(ctx context.Context, e *eventlogger.Event) (*eventl | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if e == nil { | 	if e == nil { | ||||||
| 		return nil, fmt.Errorf("%s: event is nil: %w", op, ErrInvalidParameter) | 		return nil, fmt.Errorf("event is nil: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	formattedBytes, found := e.Format(s.requiredFormat) | 	formatted, found := e.Format(s.requiredFormat) | ||||||
| 	if !found { | 	if !found { | ||||||
| 		return nil, fmt.Errorf("%s: unable to retrieve event formatted as %q", op, s.requiredFormat) | 		return nil, fmt.Errorf("unable to retrieve event formatted as %q: %w", s.requiredFormat, ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	_, err := os.Stdout.Write(formattedBytes) | 	_, err := os.Stdout.Write(formatted) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error writing to stdout: %w", op, err) | 		return nil, fmt.Errorf("error writing to stdout: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Return nil, nil to indicate the pipeline is complete. | 	// Return nil, nil to indicate the pipeline is complete. | ||||||
|   | |||||||
| @@ -23,21 +23,19 @@ type SyslogSink struct { | |||||||
| // NewSyslogSink should be used to create a new SyslogSink. | // NewSyslogSink should be used to create a new SyslogSink. | ||||||
| // Accepted options: WithFacility and WithTag. | // Accepted options: WithFacility and WithTag. | ||||||
| func NewSyslogSink(format string, opt ...Option) (*SyslogSink, error) { | func NewSyslogSink(format string, opt ...Option) (*SyslogSink, error) { | ||||||
| 	const op = "event.NewSyslogSink" |  | ||||||
|  |  | ||||||
| 	format = strings.TrimSpace(format) | 	format = strings.TrimSpace(format) | ||||||
| 	if format == "" { | 	if format == "" { | ||||||
| 		return nil, fmt.Errorf("%s: format is required: %w", op, ErrInvalidParameter) | 		return nil, fmt.Errorf("format is required: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	opts, err := getOpts(opt...) | 	opts, err := getOpts(opt...) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error applying options: %w", op, err) | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	logger, err := gsyslog.NewLogger(gsyslog.LOG_INFO, opts.withFacility, opts.withTag) | 	logger, err := gsyslog.NewLogger(gsyslog.LOG_INFO, opts.withFacility, opts.withTag) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error creating syslogger: %w", op, err) | 		return nil, fmt.Errorf("error creating syslogger: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return &SyslogSink{requiredFormat: format, logger: logger}, nil | 	return &SyslogSink{requiredFormat: format, logger: logger}, nil | ||||||
| @@ -45,8 +43,6 @@ func NewSyslogSink(format string, opt ...Option) (*SyslogSink, error) { | |||||||
|  |  | ||||||
| // Process handles writing the event to the syslog. | // Process handles writing the event to the syslog. | ||||||
| func (s *SyslogSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) { | func (s *SyslogSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) { | ||||||
| 	const op = "event.(SyslogSink).Process" |  | ||||||
|  |  | ||||||
| 	select { | 	select { | ||||||
| 	case <-ctx.Done(): | 	case <-ctx.Done(): | ||||||
| 		return nil, ctx.Err() | 		return nil, ctx.Err() | ||||||
| @@ -54,17 +50,17 @@ func (s *SyslogSink) Process(ctx context.Context, e *eventlogger.Event) (*eventl | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if e == nil { | 	if e == nil { | ||||||
| 		return nil, fmt.Errorf("%s: event is nil: %w", op, ErrInvalidParameter) | 		return nil, fmt.Errorf("event is nil: %w", ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	formatted, found := e.Format(s.requiredFormat) | 	formatted, found := e.Format(s.requiredFormat) | ||||||
| 	if !found { | 	if !found { | ||||||
| 		return nil, fmt.Errorf("%s: unable to retrieve event formatted as %q", op, s.requiredFormat) | 		return nil, fmt.Errorf("unable to retrieve event formatted as %q: %w", s.requiredFormat, ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	_, err := s.logger.Write(formatted) | 	_, err := s.logger.Write(formatted) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error writing to syslog: %w", op, err) | 		return nil, fmt.Errorf("error writing to syslog: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// return nil for the event to indicate the pipeline is complete. | 	// return nil for the event to indicate the pipeline is complete. | ||||||
|   | |||||||
| @@ -24,12 +24,12 @@ func TestNewSyslogSink(t *testing.T) { | |||||||
| 		"format-empty": { | 		"format-empty": { | ||||||
| 			format:         "", | 			format:         "", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "event.NewSyslogSink: format is required: invalid parameter", | 			expectedErrMsg: "format is required: invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"format-whitespace": { | 		"format-whitespace": { | ||||||
| 			format:         "   ", | 			format:         "   ", | ||||||
| 			wantErr:        true, | 			wantErr:        true, | ||||||
| 			expectedErrMsg: "event.NewSyslogSink: format is required: invalid parameter", | 			expectedErrMsg: "format is required: invalid parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"happy": { | 		"happy": { | ||||||
| 			format: "json", | 			format: "json", | ||||||
|   | |||||||
| @@ -473,6 +473,8 @@ github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThC | |||||||
| github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= | github.com/ryanuber/columnize v2.1.0+incompatible/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= | ||||||
| github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= | github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk= | ||||||
| github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= | github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc= | ||||||
|  | github.com/sasha-s/go-deadlock v0.2.0 h1:lMqc+fUb7RrFS3gQLtoQsJ7/6TV/pAIFvBsqX73DK8Y= | ||||||
|  | github.com/sasha-s/go-deadlock v0.2.0/go.mod h1:StQn567HiB1fF2yJ44N9au7wOhrPS3iZqiDbRupzT10= | ||||||
| github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= | github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= | ||||||
| github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= | github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= | ||||||
| github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= | github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= | ||||||
|   | |||||||
| @@ -65,6 +65,16 @@ func (c *Core) generateAuditTestProbe() (*logical.LogInput, error) { | |||||||
|  |  | ||||||
| // enableAudit is used to enable a new audit backend | // enableAudit is used to enable a new audit backend | ||||||
| func (c *Core) enableAudit(ctx context.Context, entry *MountEntry, updateStorage bool) error { | func (c *Core) enableAudit(ctx context.Context, entry *MountEntry, updateStorage bool) error { | ||||||
|  | 	// Check ahead of time if the type of audit device we're trying to enable is configured in Vault. | ||||||
|  | 	if _, ok := c.auditBackends[entry.Type]; !ok { | ||||||
|  | 		return fmt.Errorf("unknown backend type: %q: %w", entry.Type, audit.ErrExternalOptions) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// We can check early to ensure that non-Enterprise versions aren't trying to supply Enterprise only options. | ||||||
|  | 	if hasInvalidAuditOptions(entry.Options) { | ||||||
|  | 		return fmt.Errorf("enterprise-only options supplied: %w", audit.ErrExternalOptions) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Ensure we end the path in a slash | 	// Ensure we end the path in a slash | ||||||
| 	if !strings.HasSuffix(entry.Path, "/") { | 	if !strings.HasSuffix(entry.Path, "/") { | ||||||
| 		entry.Path += "/" | 		entry.Path += "/" | ||||||
| @@ -72,18 +82,13 @@ func (c *Core) enableAudit(ctx context.Context, entry *MountEntry, updateStorage | |||||||
|  |  | ||||||
| 	// Ensure there is a name | 	// Ensure there is a name | ||||||
| 	if entry.Path == "/" { | 	if entry.Path == "/" { | ||||||
| 		return fmt.Errorf("backend path must be specified") | 		return fmt.Errorf("backend path must be specified: %w", audit.ErrExternalOptions) | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// We can check early to ensure that non-Enterprise versions aren't trying to supply Enterprise only options. |  | ||||||
| 	if hasInvalidAuditOptions(entry.Options) { |  | ||||||
| 		return fmt.Errorf("enterprise-only options supplied") |  | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if fallbackRaw, ok := entry.Options["fallback"]; ok { | 	if fallbackRaw, ok := entry.Options["fallback"]; ok { | ||||||
| 		fallback, err := parseutil.ParseBool(fallbackRaw) | 		fallback, err := parseutil.ParseBool(fallbackRaw) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return fmt.Errorf("unable to enable audit device '%s', cannot parse supplied 'fallback' setting: %w", entry.Path, err) | 			return fmt.Errorf("cannot parse supplied 'fallback' setting: %w", audit.ErrExternalOptions) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Reassigning the fallback value means we can ensure that the formatting | 		// Reassigning the fallback value means we can ensure that the formatting | ||||||
| @@ -91,6 +96,17 @@ func (c *Core) enableAudit(ctx context.Context, entry *MountEntry, updateStorage | |||||||
| 		entry.Options["fallback"] = strconv.FormatBool(fallback) | 		entry.Options["fallback"] = strconv.FormatBool(fallback) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	if skipTestRaw, ok := entry.Options["skip_test"]; ok { | ||||||
|  | 		skipTest, err := parseutil.ParseBool(skipTestRaw) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return fmt.Errorf("cannot parse supplied 'skip_test' setting: %w", audit.ErrExternalOptions) | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		// Reassigning the value means we can ensure that the formatting | ||||||
|  | 		// of it as a string is consistent for future comparisons. | ||||||
|  | 		entry.Options["skip_test"] = strconv.FormatBool(skipTest) | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	// Update the audit table | 	// Update the audit table | ||||||
| 	c.auditLock.Lock() | 	c.auditLock.Lock() | ||||||
| 	defer c.auditLock.Unlock() | 	defer c.auditLock.Unlock() | ||||||
| @@ -99,13 +115,13 @@ func (c *Core) enableAudit(ctx context.Context, entry *MountEntry, updateStorage | |||||||
| 	for _, ent := range c.audit.Entries { | 	for _, ent := range c.audit.Entries { | ||||||
| 		switch { | 		switch { | ||||||
| 		case entry.Options["fallback"] == "true" && ent.Options["fallback"] == "true": | 		case entry.Options["fallback"] == "true" && ent.Options["fallback"] == "true": | ||||||
| 			return fmt.Errorf("unable to enable audit device '%s', a fallback device already exists '%s'", entry.Path, ent.Path) | 			return fmt.Errorf("fallback device already exists '%s': %w", ent.Path, audit.ErrExternalOptions) | ||||||
| 		// Existing is sql/mysql/ new is sql/ or | 		// Existing is sql/mysql/ new is sql/ or | ||||||
| 		// existing is sql/ and new is sql/mysql/ | 		// existing is sql/ and new is sql/mysql/ | ||||||
| 		case strings.HasPrefix(ent.Path, entry.Path): | 		case strings.HasPrefix(ent.Path, entry.Path): | ||||||
| 			fallthrough | 			fallthrough | ||||||
| 		case strings.HasPrefix(entry.Path, ent.Path): | 		case strings.HasPrefix(entry.Path, ent.Path): | ||||||
| 			return fmt.Errorf("path already in use") | 			return fmt.Errorf("path already in use: %w", audit.ErrExternalOptions) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -113,14 +129,14 @@ func (c *Core) enableAudit(ctx context.Context, entry *MountEntry, updateStorage | |||||||
| 	if entry.UUID == "" { | 	if entry.UUID == "" { | ||||||
| 		entryUUID, err := uuid.GenerateUUID() | 		entryUUID, err := uuid.GenerateUUID() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return fmt.Errorf("%w: %w", audit.ErrInternal, err) | ||||||
| 		} | 		} | ||||||
| 		entry.UUID = entryUUID | 		entry.UUID = entryUUID | ||||||
| 	} | 	} | ||||||
| 	if entry.Accessor == "" { | 	if entry.Accessor == "" { | ||||||
| 		accessor, err := c.generateMountAccessor("audit_" + entry.Type) | 		accessor, err := c.generateMountAccessor("audit_" + entry.Type) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return fmt.Errorf("%w: %w", audit.ErrInternal, err) | ||||||
| 		} | 		} | ||||||
| 		entry.Accessor = accessor | 		entry.Accessor = accessor | ||||||
| 	} | 	} | ||||||
| @@ -141,14 +157,14 @@ func (c *Core) enableAudit(ctx context.Context, entry *MountEntry, updateStorage | |||||||
| 		return err | 		return err | ||||||
| 	} | 	} | ||||||
| 	if backend == nil { | 	if backend == nil { | ||||||
| 		return fmt.Errorf("nil audit backend of type %q returned from factory", entry.Type) | 		return fmt.Errorf("nil audit backend of type %q returned from factory: %w", entry.Type, audit.ErrInternal) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if entry.Options["skip_test"] != "true" { | 	if entry.Options["skip_test"] != "true" { | ||||||
| 		// Test the new audit device and report failure if it doesn't work. | 		// Test the new audit device and report failure if it doesn't work. | ||||||
| 		testProbe, err := c.generateAuditTestProbe() | 		testProbe, err := c.generateAuditTestProbe() | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return fmt.Errorf("error generating test probe: %w: %w", audit.ErrInternal, err) | ||||||
| 		} | 		} | ||||||
| 		err = backend.LogTestMessage(ctx, testProbe) | 		err = backend.LogTestMessage(ctx, testProbe) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| @@ -163,14 +179,14 @@ func (c *Core) enableAudit(ctx context.Context, entry *MountEntry, updateStorage | |||||||
|  |  | ||||||
| 	ns, err := namespace.FromContext(ctx) | 	ns, err := namespace.FromContext(ctx) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return err | 		return fmt.Errorf("%w: %w", audit.ErrInternal, err) | ||||||
| 	} | 	} | ||||||
| 	entry.NamespaceID = ns.ID | 	entry.NamespaceID = ns.ID | ||||||
| 	entry.namespace = ns | 	entry.namespace = ns | ||||||
|  |  | ||||||
| 	if updateStorage { | 	if updateStorage { | ||||||
| 		if err := c.persistAudit(ctx, newTable, entry.Local); err != nil { | 		if err := c.persistAudit(ctx, newTable, entry.Local); err != nil { | ||||||
| 			return errors.New("failed to update audit table") | 			return fmt.Errorf("failed to update audit table: %w: %w", audit.ErrInternal, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -515,12 +531,12 @@ func (c *Core) removeAuditReloadFunc(entry *MountEntry) { | |||||||
| func (c *Core) newAuditBackend(ctx context.Context, entry *MountEntry, view logical.Storage, conf map[string]string) (audit.Backend, error) { | func (c *Core) newAuditBackend(ctx context.Context, entry *MountEntry, view logical.Storage, conf map[string]string) (audit.Backend, error) { | ||||||
| 	// Ensure that non-Enterprise versions aren't trying to supply Enterprise only options. | 	// Ensure that non-Enterprise versions aren't trying to supply Enterprise only options. | ||||||
| 	if hasInvalidAuditOptions(entry.Options) { | 	if hasInvalidAuditOptions(entry.Options) { | ||||||
| 		return nil, fmt.Errorf("enterprise-only options supplied") | 		return nil, fmt.Errorf("enterprise-only options supplied: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	f, ok := c.auditBackends[entry.Type] | 	f, ok := c.auditBackends[entry.Type] | ||||||
| 	if !ok { | 	if !ok { | ||||||
| 		return nil, fmt.Errorf("unknown backend type: %q", entry.Type) | 		return nil, fmt.Errorf("unknown backend type: %q: %w", entry.Type, audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
| 	saltConfig := &salt.Config{ | 	saltConfig := &salt.Config{ | ||||||
| 		HMAC:     sha256.New, | 		HMAC:     sha256.New, | ||||||
| @@ -542,7 +558,7 @@ func (c *Core) newAuditBackend(ctx context.Context, entry *MountEntry, view logi | |||||||
| 		return nil, fmt.Errorf("unable to create new audit backend: %w", err) | 		return nil, fmt.Errorf("unable to create new audit backend: %w", err) | ||||||
| 	} | 	} | ||||||
| 	if be == nil { | 	if be == nil { | ||||||
| 		return nil, fmt.Errorf("nil backend returned from %q factory function", entry.Type) | 		return nil, fmt.Errorf("nil backend returned from %q factory function: %w", entry.Type, audit.ErrInternal) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	switch entry.Type { | 	switch entry.Type { | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"errors" | 	"errors" | ||||||
| 	"fmt" | 	"fmt" | ||||||
|  | 	"reflect" | ||||||
| 	"strings" | 	"strings" | ||||||
| 	"sync" | 	"sync" | ||||||
| 	"time" | 	"time" | ||||||
| @@ -50,17 +51,15 @@ type AuditBroker struct { | |||||||
|  |  | ||||||
| // NewAuditBroker creates a new audit broker | // NewAuditBroker creates a new audit broker | ||||||
| func NewAuditBroker(log hclog.Logger) (*AuditBroker, error) { | func NewAuditBroker(log hclog.Logger) (*AuditBroker, error) { | ||||||
| 	const op = "vault.NewAuditBroker" |  | ||||||
|  |  | ||||||
| 	eventBroker, err := eventlogger.NewBroker() | 	eventBroker, err := eventlogger.NewBroker() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error creating event broker for audit events: %w", op, err) | 		return nil, fmt.Errorf("error creating event broker for audit events: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Set up the broker that will support a single fallback device. | 	// Set up the broker that will support a single fallback device. | ||||||
| 	fallbackEventBroker, err := eventlogger.NewBroker() | 	fallbackEventBroker, err := eventlogger.NewBroker() | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error creating event fallback broker for audit event: %w", op, err) | 		return nil, fmt.Errorf("error creating event fallback broker for audit event: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	broker := &AuditBroker{ | 	broker := &AuditBroker{ | ||||||
| @@ -75,45 +74,54 @@ func NewAuditBroker(log hclog.Logger) (*AuditBroker, error) { | |||||||
|  |  | ||||||
| // Register is used to add new audit backend to the broker | // Register is used to add new audit backend to the broker | ||||||
| func (a *AuditBroker) Register(name string, backend audit.Backend, local bool) error { | func (a *AuditBroker) Register(name string, backend audit.Backend, local bool) error { | ||||||
| 	const op = "vault.(AuditBroker).Register" |  | ||||||
|  |  | ||||||
| 	a.Lock() | 	a.Lock() | ||||||
| 	defer a.Unlock() | 	defer a.Unlock() | ||||||
|  |  | ||||||
| 	name = strings.TrimSpace(name) | 	name = strings.TrimSpace(name) | ||||||
| 	if name == "" { | 	if name == "" { | ||||||
| 		return fmt.Errorf("%s: name is required: %w", op, event.ErrInvalidParameter) | 		return fmt.Errorf("name is required: %w", audit.ErrInvalidParameter) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if backend == nil || reflect.ValueOf(backend).IsNil() { | ||||||
|  | 		return fmt.Errorf("backend cannot be nil: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// If the backend is already registered, we cannot re-register it. | 	// If the backend is already registered, we cannot re-register it. | ||||||
| 	if a.isRegistered(name) { | 	if a.isRegistered(name) { | ||||||
| 		return fmt.Errorf("%s: backend already registered '%s'", op, name) | 		return fmt.Errorf("backend already registered '%s': %w", name, audit.ErrExternalOptions) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Fallback devices are singleton instances, we cannot register more than one or overwrite the existing one. | 	// Fallback devices are singleton instances, we cannot register more than one or overwrite the existing one. | ||||||
| 	if backend.IsFallback() && a.fallbackBroker.IsAnyPipelineRegistered(eventlogger.EventType(event.AuditType.String())) { | 	if backend.IsFallback() && hasAuditPipelines(a.fallbackBroker) { | ||||||
| 		existing, err := a.existingFallbackName() | 		// Get the name of the fallback device which is registered with the broker. | ||||||
| 		if err != nil { | 		var existing string | ||||||
| 			return fmt.Errorf("%s: existing fallback device already registered: %w", op, err) | 		for _, be := range a.backends { | ||||||
|  | 			if be.backend.IsFallback() { | ||||||
|  | 				existing = be.backend.Name() | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		if existing == "" { | ||||||
|  | 			// We expected an existing fallback device but didn't find it. | ||||||
|  | 			return fmt.Errorf("cannot determine name of existing registered fallback device: %w", audit.ErrInternal) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		return fmt.Errorf("%s: existing fallback device already registered: %q", op, existing) | 		return fmt.Errorf("existing fallback device already registered %q: %w", existing, audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if name != backend.Name() { | 	if name != backend.Name() { | ||||||
| 		return fmt.Errorf("%s: audit registration failed due to device name mismatch: %q, %q", op, name, backend.Name()) | 		return fmt.Errorf("audit registration failed due to device name mismatch: %q, %q: %w", name, backend.Name(), audit.ErrInternal) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	switch { | 	switch { | ||||||
| 	case backend.IsFallback(): | 	case backend.IsFallback(): | ||||||
| 		err := a.registerFallback(name, backend) | 		err := a.registerFallback(name, backend) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return fmt.Errorf("%s: unable to register fallback device for %q: %w", op, name, err) | 			return fmt.Errorf("unable to register fallback device for %q: %w: %w", name, err, audit.ErrInternal) | ||||||
| 		} | 		} | ||||||
| 	default: | 	default: | ||||||
| 		err := a.register(name, backend) | 		err := a.register(name, backend) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return fmt.Errorf("%s: unable to register device for %q: %w", op, name, err) | 			return fmt.Errorf("unable to register device for %q: %w", name, err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -127,14 +135,12 @@ func (a *AuditBroker) Register(name string, backend audit.Backend, local bool) e | |||||||
|  |  | ||||||
| // Deregister is used to remove an audit backend from the broker | // Deregister is used to remove an audit backend from the broker | ||||||
| func (a *AuditBroker) Deregister(ctx context.Context, name string) error { | func (a *AuditBroker) Deregister(ctx context.Context, name string) error { | ||||||
| 	const op = "vault.(AuditBroker).Deregister" |  | ||||||
|  |  | ||||||
| 	a.Lock() | 	a.Lock() | ||||||
| 	defer a.Unlock() | 	defer a.Unlock() | ||||||
|  |  | ||||||
| 	name = strings.TrimSpace(name) | 	name = strings.TrimSpace(name) | ||||||
| 	if name == "" { | 	if name == "" { | ||||||
| 		return fmt.Errorf("%s: name is required: %w", op, event.ErrInvalidParameter) | 		return fmt.Errorf("name is required: %w", audit.ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// If the backend isn't actually registered, then there's nothing to do. | 	// If the backend isn't actually registered, then there's nothing to do. | ||||||
| @@ -148,17 +154,16 @@ func (a *AuditBroker) Deregister(ctx context.Context, name string) error { | |||||||
| 	// the error. | 	// the error. | ||||||
| 	delete(a.backends, name) | 	delete(a.backends, name) | ||||||
|  |  | ||||||
|  | 	var err error | ||||||
| 	switch { | 	switch { | ||||||
| 	case name == a.fallbackName: | 	case name == a.fallbackName: | ||||||
| 		err := a.deregisterFallback(ctx, name) | 		err = a.deregisterFallback(ctx, name) | ||||||
| 		if err != nil { |  | ||||||
| 			return fmt.Errorf("%s: deregistration failed for fallback audit device %q: %w", op, name, err) |  | ||||||
| 		} |  | ||||||
| 	default: | 	default: | ||||||
| 		err := a.deregister(ctx, name) | 		err = a.deregister(ctx, name) | ||||||
| 		if err != nil { | 	} | ||||||
| 			return fmt.Errorf("%s: deregistration failed for audit device %q: %w", op, name, err) |  | ||||||
| 		} | 	if err != nil { | ||||||
|  | 		return fmt.Errorf("deregistration failed for audit device %q: %w: %w", name, err, audit.ErrInternal) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| @@ -239,8 +244,8 @@ func (a *AuditBroker) LogRequest(ctx context.Context, in *logical.LogInput) (ret | |||||||
| 	// normal audit devices, so check if the broker had an audit based pipeline | 	// normal audit devices, so check if the broker had an audit based pipeline | ||||||
| 	// registered before trying to send to it. | 	// registered before trying to send to it. | ||||||
| 	var status eventlogger.Status | 	var status eventlogger.Status | ||||||
| 	if a.broker.IsAnyPipelineRegistered(eventlogger.EventType(event.AuditType.String())) { | 	if hasAuditPipelines(a.broker) { | ||||||
| 		status, err = a.broker.Send(ctx, eventlogger.EventType(event.AuditType.String()), e) | 		status, err = a.broker.Send(ctx, event.AuditType.AsEventType(), e) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			retErr = multierror.Append(retErr, multierror.Append(err, status.Warnings...)) | 			retErr = multierror.Append(retErr, multierror.Append(err, status.Warnings...)) | ||||||
| 			return retErr.ErrorOrNil() | 			return retErr.ErrorOrNil() | ||||||
| @@ -260,8 +265,8 @@ func (a *AuditBroker) LogRequest(ctx context.Context, in *logical.LogInput) (ret | |||||||
|  |  | ||||||
| 	// If a fallback device is registered we can rely on that to 'catch all' | 	// If a fallback device is registered we can rely on that to 'catch all' | ||||||
| 	// and also the broker level guarantee for completed sinks. | 	// and also the broker level guarantee for completed sinks. | ||||||
| 	if a.fallbackBroker.IsAnyPipelineRegistered(eventlogger.EventType(event.AuditType.String())) { | 	if a.fallbackBroker.IsAnyPipelineRegistered(event.AuditType.AsEventType()) { | ||||||
| 		status, err = a.fallbackBroker.Send(ctx, eventlogger.EventType(event.AuditType.String()), e) | 		status, err = a.fallbackBroker.Send(ctx, event.AuditType.AsEventType(), e) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			retErr = multierror.Append(retErr, multierror.Append(fmt.Errorf("auditing request to fallback device failed: %w", err), status.Warnings...)) | 			retErr = multierror.Append(retErr, multierror.Append(fmt.Errorf("auditing request to fallback device failed: %w", err), status.Warnings...)) | ||||||
| 		} | 		} | ||||||
| @@ -323,8 +328,8 @@ func (a *AuditBroker) LogResponse(ctx context.Context, in *logical.LogInput) (re | |||||||
| 	// normal audit devices, so check if the broker had an audit based pipeline | 	// normal audit devices, so check if the broker had an audit based pipeline | ||||||
| 	// registered before trying to send to it. | 	// registered before trying to send to it. | ||||||
| 	var status eventlogger.Status | 	var status eventlogger.Status | ||||||
| 	if a.broker.IsAnyPipelineRegistered(eventlogger.EventType(event.AuditType.String())) { | 	if a.broker.IsAnyPipelineRegistered(event.AuditType.AsEventType()) { | ||||||
| 		status, err = a.broker.Send(auditContext, eventlogger.EventType(event.AuditType.String()), e) | 		status, err = a.broker.Send(auditContext, event.AuditType.AsEventType(), e) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			retErr = multierror.Append(retErr, multierror.Append(err, status.Warnings...)) | 			retErr = multierror.Append(retErr, multierror.Append(err, status.Warnings...)) | ||||||
| 			return retErr.ErrorOrNil() | 			return retErr.ErrorOrNil() | ||||||
| @@ -344,8 +349,8 @@ func (a *AuditBroker) LogResponse(ctx context.Context, in *logical.LogInput) (re | |||||||
|  |  | ||||||
| 	// If a fallback device is registered we can rely on that to 'catch all' | 	// If a fallback device is registered we can rely on that to 'catch all' | ||||||
| 	// and also the broker level guarantee for completed sinks. | 	// and also the broker level guarantee for completed sinks. | ||||||
| 	if a.fallbackBroker.IsAnyPipelineRegistered(eventlogger.EventType(event.AuditType.String())) { | 	if a.fallbackBroker.IsAnyPipelineRegistered(event.AuditType.AsEventType()) { | ||||||
| 		status, err = a.fallbackBroker.Send(auditContext, eventlogger.EventType(event.AuditType.String()), e) | 		status, err = a.fallbackBroker.Send(auditContext, event.AuditType.AsEventType(), e) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			retErr = multierror.Append(retErr, multierror.Append(fmt.Errorf("auditing response to fallback device failed: %w", err), status.Warnings...)) | 			retErr = multierror.Append(retErr, multierror.Append(fmt.Errorf("auditing response to fallback device failed: %w", err), status.Warnings...)) | ||||||
| 		} | 		} | ||||||
| @@ -400,12 +405,10 @@ func (a *AuditBroker) requiredSuccessThresholdSinks() int { | |||||||
| // backend's name, on the specified eventlogger.Broker using the audit.Backend | // backend's name, on the specified eventlogger.Broker using the audit.Backend | ||||||
| // to supply them. | // to supply them. | ||||||
| func registerNodesAndPipeline(broker *eventlogger.Broker, b audit.Backend) error { | func registerNodesAndPipeline(broker *eventlogger.Broker, b audit.Backend) error { | ||||||
| 	const op = "vault.registerNodesAndPipeline" |  | ||||||
|  |  | ||||||
| 	for id, node := range b.Nodes() { | 	for id, node := range b.Nodes() { | ||||||
| 		err := broker.RegisterNode(id, node) | 		err := broker.RegisterNode(id, node) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return fmt.Errorf("%s: unable to register nodes for %q: %w", op, b.Name(), err) | 			return fmt.Errorf("unable to register nodes for %q: %w", b.Name(), err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -417,44 +420,27 @@ func registerNodesAndPipeline(broker *eventlogger.Broker, b audit.Backend) error | |||||||
|  |  | ||||||
| 	err := broker.RegisterPipeline(pipeline) | 	err := broker.RegisterPipeline(pipeline) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to register pipeline for %q: %w", op, b.Name(), err) | 		return fmt.Errorf("unable to register pipeline for %q: %w", b.Name(), err) | ||||||
| 	} | 	} | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // existingFallbackName returns the name of the fallback device which is registered |  | ||||||
| // with the AuditBroker. |  | ||||||
| func (a *AuditBroker) existingFallbackName() (string, error) { |  | ||||||
| 	const op = "vault.(AuditBroker).existingFallbackName" |  | ||||||
|  |  | ||||||
| 	for _, be := range a.backends { |  | ||||||
| 		if be.backend.IsFallback() { |  | ||||||
| 			return be.backend.Name(), nil |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return "", fmt.Errorf("%s: existing fallback device name is missing", op) |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // registerFallback can be used to register a fallback device, it will also | // registerFallback can be used to register a fallback device, it will also | ||||||
| // configure the success threshold required for sinks. | // configure the success threshold required for sinks. | ||||||
| func (a *AuditBroker) registerFallback(name string, backend audit.Backend) error { | func (a *AuditBroker) registerFallback(name string, backend audit.Backend) error { | ||||||
| 	const op = "vault.(AuditBroker).registerFallback" |  | ||||||
|  |  | ||||||
| 	err := registerNodesAndPipeline(a.fallbackBroker, backend) | 	err := registerNodesAndPipeline(a.fallbackBroker, backend) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: fallback device pipeline registration error: %w", op, err) | 		return fmt.Errorf("pipeline registration error for fallback device: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Store the name of the fallback audit device so that we can check when | 	// Store the name of the fallback audit device so that we can check when | ||||||
| 	// deregistering if the device is the single fallback one. | 	// deregistering if the device is the single fallback one. | ||||||
| 	a.fallbackName = backend.Name() | 	a.fallbackName = backend.Name() | ||||||
|  |  | ||||||
| 	// We need to turn on the threshold for the fallback broker, so we can | 	// We need to turn on the threshold for the fallback broker, so we can guarantee it ends up somewhere | ||||||
| 	// guarantee it ends up somewhere | 	err = a.fallbackBroker.SetSuccessThresholdSinks(event.AuditType.AsEventType(), 1) | ||||||
| 	err = a.fallbackBroker.SetSuccessThresholdSinks(eventlogger.EventType(event.AuditType.String()), 1) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to configure fallback sink success threshold (1) for %q: %w", op, name, err) | 		return fmt.Errorf("unable to configure fallback sink success threshold (1): %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| @@ -463,16 +449,14 @@ func (a *AuditBroker) registerFallback(name string, backend audit.Backend) error | |||||||
| // deregisterFallback can be used to deregister a fallback audit device, it will | // deregisterFallback can be used to deregister a fallback audit device, it will | ||||||
| // also configure the success threshold required for sinks. | // also configure the success threshold required for sinks. | ||||||
| func (a *AuditBroker) deregisterFallback(ctx context.Context, name string) error { | func (a *AuditBroker) deregisterFallback(ctx context.Context, name string) error { | ||||||
| 	const op = "vault.(AuditBroker).deregisterFallback" | 	err := a.fallbackBroker.SetSuccessThresholdSinks(event.AuditType.AsEventType(), 0) | ||||||
|  |  | ||||||
| 	err := a.fallbackBroker.SetSuccessThresholdSinks(eventlogger.EventType(event.AuditType.String()), 0) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to configure fallback sink success threshold (0) for %q: %w", op, name, err) | 		return fmt.Errorf("unable to reconfigure fallback sink success threshold (0): %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	_, err = a.fallbackBroker.RemovePipelineAndNodes(ctx, eventlogger.EventType(event.AuditType.String()), eventlogger.PipelineID(name)) | 	_, err = a.fallbackBroker.RemovePipelineAndNodes(ctx, event.AuditType.AsEventType(), eventlogger.PipelineID(name)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to deregister fallback device %q: %w", op, name, err) | 		return fmt.Errorf("unable to deregister fallback device: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Clear the fallback device name now we've deregistered. | 	// Clear the fallback device name now we've deregistered. | ||||||
| @@ -484,11 +468,9 @@ func (a *AuditBroker) deregisterFallback(ctx context.Context, name string) error | |||||||
| // register can be used to register a normal audit device, it will also calculate | // register can be used to register a normal audit device, it will also calculate | ||||||
| // and configure the success threshold required for sinks. | // and configure the success threshold required for sinks. | ||||||
| func (a *AuditBroker) register(name string, backend audit.Backend) error { | func (a *AuditBroker) register(name string, backend audit.Backend) error { | ||||||
| 	const op = "vault.(AuditBroker).register" |  | ||||||
|  |  | ||||||
| 	err := registerNodesAndPipeline(a.broker, backend) | 	err := registerNodesAndPipeline(a.broker, backend) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: audit pipeline registration error: %w", op, err) | 		return fmt.Errorf("audit pipeline registration error: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Establish if we ONLY have pipelines that include filter nodes. | 	// Establish if we ONLY have pipelines that include filter nodes. | ||||||
| @@ -503,9 +485,9 @@ func (a *AuditBroker) register(name string, backend audit.Backend) error { | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Update the success threshold now that the pipeline is registered. | 	// Update the success threshold now that the pipeline is registered. | ||||||
| 	err = a.broker.SetSuccessThresholdSinks(eventlogger.EventType(event.AuditType.String()), threshold) | 	err = a.broker.SetSuccessThresholdSinks(event.AuditType.AsEventType(), threshold) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to configure sink success threshold (%d) for %q: %w", op, threshold, name, err) | 		return fmt.Errorf("unable to configure sink success threshold (%d): %w", threshold, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| @@ -514,25 +496,29 @@ func (a *AuditBroker) register(name string, backend audit.Backend) error { | |||||||
| // deregister can be used to deregister a normal audit device, it will also | // deregister can be used to deregister a normal audit device, it will also | ||||||
| // calculate and configure the success threshold required for sinks. | // calculate and configure the success threshold required for sinks. | ||||||
| func (a *AuditBroker) deregister(ctx context.Context, name string) error { | func (a *AuditBroker) deregister(ctx context.Context, name string) error { | ||||||
| 	const op = "vault.(AuditBroker).deregister" |  | ||||||
|  |  | ||||||
| 	// Establish if we ONLY have pipelines that include filter nodes. | 	// Establish if we ONLY have pipelines that include filter nodes. | ||||||
| 	// Otherwise, we can rely on the eventlogger broker guarantee. | 	// Otherwise, we can rely on the eventlogger broker guarantee. | ||||||
| 	threshold := a.requiredSuccessThresholdSinks() | 	threshold := a.requiredSuccessThresholdSinks() | ||||||
|  |  | ||||||
| 	err := a.broker.SetSuccessThresholdSinks(eventlogger.EventType(event.AuditType.String()), threshold) | 	err := a.broker.SetSuccessThresholdSinks(event.AuditType.AsEventType(), threshold) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to configure sink success threshold (%d) for %q: %w", op, threshold, name, err) | 		return fmt.Errorf("unable to reconfigure sink success threshold (%d): %w", threshold, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// The first return value, a bool, indicates whether | 	// The first return value, a bool, indicates whether | ||||||
| 	// RemovePipelineAndNodes encountered the error while evaluating | 	// RemovePipelineAndNodes encountered the error while evaluating | ||||||
| 	// pre-conditions (false) or once it started removing the pipeline and | 	// pre-conditions (false) or once it started removing the pipeline and | ||||||
| 	// the nodes (true). This code doesn't care either way. | 	// the nodes (true). This code doesn't care either way. | ||||||
| 	_, err = a.broker.RemovePipelineAndNodes(ctx, eventlogger.EventType(event.AuditType.String()), eventlogger.PipelineID(name)) | 	_, err = a.broker.RemovePipelineAndNodes(ctx, event.AuditType.AsEventType(), eventlogger.PipelineID(name)) | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return fmt.Errorf("%s: unable to remove pipeline and nodes for %q: %w", op, name, err) | 		return fmt.Errorf("unable to remove pipeline and nodes: %w", err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // hasAuditPipelines can be used as a shorthand to check if a broker has any | ||||||
|  | // registered pipelines that are for the audit event type. | ||||||
|  | func hasAuditPipelines(broker *eventlogger.Broker) bool { | ||||||
|  | 	return broker.IsAnyPipelineRegistered(event.AuditType.AsEventType()) | ||||||
|  | } | ||||||
|   | |||||||
| @@ -89,7 +89,7 @@ func TestAuditBroker_Register_MultipleFails(t *testing.T) { | |||||||
|  |  | ||||||
| 	err = a.Register(path, noFilterBackend, false) | 	err = a.Register(path, noFilterBackend, false) | ||||||
| 	require.Error(t, err) | 	require.Error(t, err) | ||||||
| 	require.EqualError(t, err, "vault.(AuditBroker).Register: backend already registered 'b2-no-filter'") | 	require.EqualError(t, err, "backend already registered 'b2-no-filter': invalid configuration") | ||||||
| } | } | ||||||
|  |  | ||||||
| // BenchmarkAuditBroker_File_Request_DevNull Attempts to register a single `file` | // BenchmarkAuditBroker_File_Request_DevNull Attempts to register a single `file` | ||||||
|   | |||||||
| @@ -32,6 +32,7 @@ import ( | |||||||
| 	"github.com/hashicorp/go-secure-stdlib/parseutil" | 	"github.com/hashicorp/go-secure-stdlib/parseutil" | ||||||
| 	"github.com/hashicorp/go-secure-stdlib/strutil" | 	"github.com/hashicorp/go-secure-stdlib/strutil" | ||||||
| 	semver "github.com/hashicorp/go-version" | 	semver "github.com/hashicorp/go-version" | ||||||
|  | 	"github.com/hashicorp/vault/audit" | ||||||
| 	"github.com/hashicorp/vault/helper/experiments" | 	"github.com/hashicorp/vault/helper/experiments" | ||||||
| 	"github.com/hashicorp/vault/helper/hostutil" | 	"github.com/hashicorp/vault/helper/hostutil" | ||||||
| 	"github.com/hashicorp/vault/helper/identity" | 	"github.com/hashicorp/vault/helper/identity" | ||||||
| @@ -3865,7 +3866,7 @@ func (b *SystemBackend) handleAuditHash(ctx context.Context, req *logical.Reques | |||||||
| } | } | ||||||
|  |  | ||||||
| // handleEnableAudit is used to enable a new audit backend | // handleEnableAudit is used to enable a new audit backend | ||||||
| func (b *SystemBackend) handleEnableAudit(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | func (b *SystemBackend) handleEnableAudit(ctx context.Context, _ *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||||
| 	repState := b.Core.ReplicationState() | 	repState := b.Core.ReplicationState() | ||||||
|  |  | ||||||
| 	local := data.Get("local").(bool) | 	local := data.Get("local").(bool) | ||||||
| @@ -3895,13 +3896,14 @@ func (b *SystemBackend) handleEnableAudit(ctx context.Context, req *logical.Requ | |||||||
| 	// Attempt enabling | 	// Attempt enabling | ||||||
| 	if err := b.Core.enableAudit(ctx, me, true); err != nil { | 	if err := b.Core.enableAudit(ctx, me, true); err != nil { | ||||||
| 		b.Backend.Logger().Error("enable audit mount failed", "path", me.Path, "error", err) | 		b.Backend.Logger().Error("enable audit mount failed", "path", me.Path, "error", err) | ||||||
| 		return handleError(err) |  | ||||||
|  | 		return handleError(audit.ConvertToExternalError(err)) | ||||||
| 	} | 	} | ||||||
| 	return nil, nil | 	return nil, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // handleDisableAudit is used to disable an audit backend | // handleDisableAudit is used to disable an audit backend | ||||||
| func (b *SystemBackend) handleDisableAudit(ctx context.Context, req *logical.Request, data *framework.FieldData) (*logical.Response, error) { | func (b *SystemBackend) handleDisableAudit(ctx context.Context, _ *logical.Request, data *framework.FieldData) (*logical.Response, error) { | ||||||
| 	path := data.Get("path").(string) | 	path := data.Get("path").(string) | ||||||
|  |  | ||||||
| 	if !strings.HasSuffix(path, "/") { | 	if !strings.HasSuffix(path, "/") { | ||||||
| @@ -3936,7 +3938,8 @@ func (b *SystemBackend) handleDisableAudit(ctx context.Context, req *logical.Req | |||||||
| 	// Attempt disable | 	// Attempt disable | ||||||
| 	if existed, err := b.Core.disableAudit(ctx, path, true); existed && err != nil { | 	if existed, err := b.Core.disableAudit(ctx, path, true); existed && err != nil { | ||||||
| 		b.Backend.Logger().Error("disable audit mount failed", "path", path, "error", err) | 		b.Backend.Logger().Error("disable audit mount failed", "path", path, "error", err) | ||||||
| 		return handleError(err) |  | ||||||
|  | 		return handleError(audit.ConvertToExternalError(err)) | ||||||
| 	} | 	} | ||||||
| 	return nil, nil | 	return nil, nil | ||||||
| } | } | ||||||
|   | |||||||
| @@ -2792,7 +2792,7 @@ func TestSystemBackend_enableAudit_invalid(t *testing.T) { | |||||||
| 	if err != logical.ErrInvalidRequest { | 	if err != logical.ErrInvalidRequest { | ||||||
| 		t.Fatalf("err: %v", err) | 		t.Fatalf("err: %v", err) | ||||||
| 	} | 	} | ||||||
| 	if resp.Data["error"] != `unknown backend type: "nope"` { | 	if resp.Data["error"] != "unknown backend type: \"nope\": invalid configuration" { | ||||||
| 		t.Fatalf("bad: %v", resp) | 		t.Fatalf("bad: %v", resp) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Peter Wilson
					Peter Wilson