mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-30 18:17:55 +00:00 
			
		
		
		
	VAULT-17075: syslog sink node (#21859)
* syslog sink added, options + tests added, tweaks to file sink comments * defaults for syslog options
This commit is contained in:
		| @@ -34,6 +34,7 @@ type AuditFileSink struct { | |||||||
| } | } | ||||||
|  |  | ||||||
| // NewAuditFileSink should be used to create a new AuditFileSink. | // NewAuditFileSink should be used to create a new AuditFileSink. | ||||||
|  | // Accepted options: WithFileMode and WithPrefix. | ||||||
| func NewAuditFileSink(path string, format auditFormat, opt ...Option) (*AuditFileSink, error) { | func NewAuditFileSink(path string, format auditFormat, opt ...Option) (*AuditFileSink, error) { | ||||||
| 	const op = "event.NewAuditFileSink" | 	const op = "event.NewAuditFileSink" | ||||||
|  |  | ||||||
| @@ -110,12 +111,12 @@ func (f *AuditFileSink) Process(ctx context.Context, e *eventlogger.Event) (*eve | |||||||
| 		return nil, fmt.Errorf("%s: unable to retrieve event formatted as %q", op, f.format) | 		return nil, fmt.Errorf("%s: unable to retrieve event formatted as %q", op, f.format) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	buffer := bytes.NewBuffer(formatted) | 	err := f.log(formatted) | ||||||
| 	err := f.log(buffer) |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("%s: error writing file for audit sink: %w", op, err) | 		return nil, fmt.Errorf("%s: error writing file for audit sink: %w", op, err) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
|  | 	// return nil for the event to indicate the pipeline is complete. | ||||||
| 	return nil, nil | 	return nil, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -145,7 +146,7 @@ func (f *AuditFileSink) Reopen() error { | |||||||
| 	return f.open() | 	return f.open() | ||||||
| } | } | ||||||
|  |  | ||||||
| // Type is used to define which type of node AuditFileSink is. | // Type describes the type of this node (sink). | ||||||
| func (f *AuditFileSink) Type() eventlogger.NodeType { | func (f *AuditFileSink) Type() eventlogger.NodeType { | ||||||
| 	return eventlogger.NodeTypeSink | 	return eventlogger.NodeTypeSink | ||||||
| } | } | ||||||
| @@ -189,13 +190,13 @@ func (f *AuditFileSink) 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 (f *AuditFileSink) log(buf *bytes.Buffer) error { | func (f *AuditFileSink) log(data []byte) error { | ||||||
| 	const op = "event.(AuditFileSink).log" | 	const op = "event.(AuditFileSink).log" | ||||||
|  |  | ||||||
| 	f.fileLock.Lock() | 	f.fileLock.Lock() | ||||||
| 	defer f.fileLock.Unlock() | 	defer f.fileLock.Unlock() | ||||||
|  |  | ||||||
| 	reader := bytes.NewReader(buf.Bytes()) | 	reader := bytes.NewReader(data) | ||||||
|  |  | ||||||
| 	var writer io.Writer | 	var writer io.Writer | ||||||
| 	switch { | 	switch { | ||||||
|   | |||||||
							
								
								
									
										75
									
								
								internal/observability/event/audit_sink_syslog.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								internal/observability/event/audit_sink_syslog.go
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | |||||||
|  | // Copyright (c) HashiCorp, Inc. | ||||||
|  | // SPDX-License-Identifier: MPL-2.0 | ||||||
|  |  | ||||||
|  | package event | ||||||
|  |  | ||||||
|  | import ( | ||||||
|  | 	"context" | ||||||
|  | 	"fmt" | ||||||
|  |  | ||||||
|  | 	gsyslog "github.com/hashicorp/go-syslog" | ||||||
|  |  | ||||||
|  | 	"github.com/hashicorp/eventlogger" | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | // AuditSyslogSink is a sink node which handles writing audit events to syslog. | ||||||
|  | type AuditSyslogSink struct { | ||||||
|  | 	format auditFormat | ||||||
|  | 	logger gsyslog.Syslogger | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // NewAuditSyslogSink should be used to create a new AuditSyslogSink. | ||||||
|  | // Accepted options: WithFacility and WithTag. | ||||||
|  | func NewAuditSyslogSink(format auditFormat, opt ...Option) (*AuditSyslogSink, error) { | ||||||
|  | 	const op = "event.NewAuditSyslogSink" | ||||||
|  |  | ||||||
|  | 	opts, err := getOpts(opt...) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("%s: error applying options: %w", op, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	logger, err := gsyslog.NewLogger(gsyslog.LOG_INFO, opts.withFacility, opts.withTag) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("%s: error creating syslogger: %w", op, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &AuditSyslogSink{format: format, logger: logger}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Process handles writing the event to the syslog. | ||||||
|  | func (s *AuditSyslogSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) { | ||||||
|  | 	const op = "event.(AuditSyslogSink).Process" | ||||||
|  |  | ||||||
|  | 	select { | ||||||
|  | 	case <-ctx.Done(): | ||||||
|  | 		return nil, ctx.Err() | ||||||
|  | 	default: | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if e == nil { | ||||||
|  | 		return nil, fmt.Errorf("%s: event is nil: %w", op, ErrInvalidParameter) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	formatted, found := e.Format(s.format.String()) | ||||||
|  | 	if !found { | ||||||
|  | 		return nil, fmt.Errorf("%s: unable to retrieve event formatted as %q", op, s.format) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	_, err := s.logger.Write(formatted) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("%s: error writing to syslog: %w", op, err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// return nil for the event to indicate the pipeline is complete. | ||||||
|  | 	return nil, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Reopen is a no-op for a syslog sink. | ||||||
|  | func (s *AuditSyslogSink) Reopen() error { | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // Type describes the type of this node (sink). | ||||||
|  | func (s *AuditSyslogSink) Type() eventlogger.NodeType { | ||||||
|  | 	return eventlogger.NodeTypeSink | ||||||
|  | } | ||||||
| @@ -25,12 +25,16 @@ type options struct { | |||||||
| 	withFormat   auditFormat | 	withFormat   auditFormat | ||||||
| 	withFileMode *os.FileMode | 	withFileMode *os.FileMode | ||||||
| 	withPrefix   string | 	withPrefix   string | ||||||
|  | 	withFacility string | ||||||
|  | 	withTag      string | ||||||
| } | } | ||||||
|  |  | ||||||
| // getDefaultOptions returns options with their default values. | // getDefaultOptions returns options with their default values. | ||||||
| func getDefaultOptions() options { | func getDefaultOptions() options { | ||||||
| 	return options{ | 	return options{ | ||||||
| 		withNow: time.Now(), | 		withNow:      time.Now(), | ||||||
|  | 		withFacility: "AUTH", | ||||||
|  | 		withTag:      "vault", | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -143,12 +147,11 @@ func WithFormat(format string) Option { | |||||||
| // applied, but it will not return an error in those circumstances. | // applied, but it will not return an error in those circumstances. | ||||||
| func WithFileMode(mode string) Option { | func WithFileMode(mode string) Option { | ||||||
| 	return func(o *options) error { | 	return func(o *options) error { | ||||||
| 		// Clear up whitespace before attempting to parse | 		// If supplied file mode is empty, just return early without setting anything. | ||||||
|  | 		// We can assume that this option was called by something that didn't | ||||||
|  | 		// parse the incoming value, perhaps from a config map etc. | ||||||
| 		mode = strings.TrimSpace(mode) | 		mode = strings.TrimSpace(mode) | ||||||
| 		if mode == "" { | 		if mode == "" { | ||||||
| 			// If supplied file mode is empty, just return early without setting anything. |  | ||||||
| 			// We can assume that this option was called by something that didn't |  | ||||||
| 			// parse the incoming value, perhaps from a config map etc. |  | ||||||
| 			return nil | 			return nil | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| @@ -175,3 +178,29 @@ func WithPrefix(prefix string) Option { | |||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // WithFacility provides an option to represent a 'facility' for a syslog sink. | ||||||
|  | func WithFacility(facility string) Option { | ||||||
|  | 	return func(o *options) error { | ||||||
|  | 		facility = strings.TrimSpace(facility) | ||||||
|  |  | ||||||
|  | 		if facility != "" { | ||||||
|  | 			o.withFacility = facility | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // WithTag provides an option to represent a 'tag' for a syslog sink. | ||||||
|  | func WithTag(tag string) Option { | ||||||
|  | 	return func(o *options) error { | ||||||
|  | 		tag = strings.TrimSpace(tag) | ||||||
|  |  | ||||||
|  | 		if tag != "" { | ||||||
|  | 			o.withTag = tag | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		return nil | ||||||
|  | 	} | ||||||
|  | } | ||||||
|   | |||||||
| @@ -197,6 +197,82 @@ func TestOptions_WithID(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // TestOptions_WithFacility exercises WithFacility option to ensure it performs as expected. | ||||||
|  | func TestOptions_WithFacility(t *testing.T) { | ||||||
|  | 	tests := map[string]struct { | ||||||
|  | 		Value         string | ||||||
|  | 		ExpectedValue string | ||||||
|  | 	}{ | ||||||
|  | 		"empty": { | ||||||
|  | 			Value:         "", | ||||||
|  | 			ExpectedValue: "", | ||||||
|  | 		}, | ||||||
|  | 		"whitespace": { | ||||||
|  | 			Value:         "    ", | ||||||
|  | 			ExpectedValue: "", | ||||||
|  | 		}, | ||||||
|  | 		"value": { | ||||||
|  | 			Value:         "juan", | ||||||
|  | 			ExpectedValue: "juan", | ||||||
|  | 		}, | ||||||
|  | 		"spacey-value": { | ||||||
|  | 			Value:         "   juan   ", | ||||||
|  | 			ExpectedValue: "juan", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for name, tc := range tests { | ||||||
|  | 		name := name | ||||||
|  | 		tc := tc | ||||||
|  | 		t.Run(name, func(t *testing.T) { | ||||||
|  | 			t.Parallel() | ||||||
|  | 			options := &options{} | ||||||
|  | 			applyOption := WithFacility(tc.Value) | ||||||
|  | 			err := applyOption(options) | ||||||
|  | 			require.NoError(t, err) | ||||||
|  | 			require.Equal(t, tc.ExpectedValue, options.withFacility) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // TestOptions_WithTag exercises WithTag option to ensure it performs as expected. | ||||||
|  | func TestOptions_WithTag(t *testing.T) { | ||||||
|  | 	tests := map[string]struct { | ||||||
|  | 		Value         string | ||||||
|  | 		ExpectedValue string | ||||||
|  | 	}{ | ||||||
|  | 		"empty": { | ||||||
|  | 			Value:         "", | ||||||
|  | 			ExpectedValue: "", | ||||||
|  | 		}, | ||||||
|  | 		"whitespace": { | ||||||
|  | 			Value:         "    ", | ||||||
|  | 			ExpectedValue: "", | ||||||
|  | 		}, | ||||||
|  | 		"value": { | ||||||
|  | 			Value:         "juan", | ||||||
|  | 			ExpectedValue: "juan", | ||||||
|  | 		}, | ||||||
|  | 		"spacey-value": { | ||||||
|  | 			Value:         "   juan   ", | ||||||
|  | 			ExpectedValue: "juan", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	for name, tc := range tests { | ||||||
|  | 		name := name | ||||||
|  | 		tc := tc | ||||||
|  | 		t.Run(name, func(t *testing.T) { | ||||||
|  | 			t.Parallel() | ||||||
|  | 			options := &options{} | ||||||
|  | 			applyOption := WithTag(tc.Value) | ||||||
|  | 			err := applyOption(options) | ||||||
|  | 			require.NoError(t, err) | ||||||
|  | 			require.Equal(t, tc.ExpectedValue, options.withTag) | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  |  | ||||||
| // TestOptions_WithFileMode exercises WithFileMode option to ensure it performs as expected. | // TestOptions_WithFileMode exercises WithFileMode option to ensure it performs as expected. | ||||||
| func TestOptions_WithFileMode(t *testing.T) { | func TestOptions_WithFileMode(t *testing.T) { | ||||||
| 	tests := map[string]struct { | 	tests := map[string]struct { | ||||||
| @@ -266,6 +342,8 @@ func TestOptions_Default(t *testing.T) { | |||||||
| 	require.NotNil(t, opts) | 	require.NotNil(t, opts) | ||||||
| 	require.True(t, time.Now().After(opts.withNow)) | 	require.True(t, time.Now().After(opts.withNow)) | ||||||
| 	require.False(t, opts.withNow.IsZero()) | 	require.False(t, opts.withNow.IsZero()) | ||||||
|  | 	require.Equal(t, "AUTH", opts.withFacility) | ||||||
|  | 	require.Equal(t, "vault", opts.withTag) | ||||||
| } | } | ||||||
|  |  | ||||||
| // TestOptions_Opts exercises getOpts with various Option values. | // TestOptions_Opts exercises getOpts with various Option values. | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Peter Wilson
					Peter Wilson