Skip to content

Commit

Permalink
move noop audit to audit package (#26448)
Browse files Browse the repository at this point in the history
  • Loading branch information
Peter Wilson authored Apr 16, 2024
1 parent 76be7fb commit 38a7869
Show file tree
Hide file tree
Showing 25 changed files with 435 additions and 468 deletions.
350 changes: 350 additions & 0 deletions audit/backend_noop.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,350 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1

package audit

import (
"context"
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"testing"

"github.com/hashicorp/eventlogger"
"github.com/hashicorp/vault/helper/testhelpers/corehelpers"
"github.com/hashicorp/vault/internal/observability/event"
"github.com/hashicorp/vault/sdk/helper/salt"
"github.com/hashicorp/vault/sdk/logical"
)

var (
_ Backend = (*NoopAudit)(nil)
_ eventlogger.Node = (*noopWrapper)(nil)
)

// noopWrapper is designed to wrap a formatter node in order to allow access to
// bytes formatted, headers formatted and parts of the logical.LogInput.
// Some older tests relied on being able to query this information so while those
// tests stick around we should look after them.
type noopWrapper struct {
format string
node eventlogger.Node
backend *NoopAudit
}

// NoopAuditEventListener is a callback used by noopWrapper.Process() to notify
// of each received audit event.
type NoopAuditEventListener func(*AuditEvent)

func (n *NoopAudit) SetListener(listener NoopAuditEventListener) {
n.listener = listener
}

type NoopAudit struct {
Config *BackendConfig

ReqErr error
ReqAuth []*logical.Auth
Req []*logical.Request
ReqHeaders []map[string][]string
ReqNonHMACKeys []string
ReqErrs []error

RespErr error
RespAuth []*logical.Auth
RespReq []*logical.Request
Resp []*logical.Response
RespNonHMACKeys [][]string
RespReqNonHMACKeys [][]string
RespErrs []error
records [][]byte
l sync.RWMutex
salt *salt.Salt
saltMutex sync.RWMutex

nodeIDList []eventlogger.NodeID
nodeMap map[eventlogger.NodeID]eventlogger.Node

listener NoopAuditEventListener
}

// NoopHeaderFormatter can be used within no-op audit devices to do nothing when
// it comes to only allow configured headers to appear in the result.
// Whatever is passed in will be returned (nil becomes an empty map) in lowercase.
type NoopHeaderFormatter struct{}

// ApplyConfig implements the relevant interface to make NoopHeaderFormatter an HeaderFormatter.
func (f *NoopHeaderFormatter) ApplyConfig(_ context.Context, headers map[string][]string, _ Salter) (result map[string][]string, retErr error) {
if len(headers) < 1 {
return map[string][]string{}, nil
}

// Make a copy of the incoming headers with everything lower so we can
// case-insensitively compare
lowerHeaders := make(map[string][]string, len(headers))
for k, v := range headers {
lowerHeaders[strings.ToLower(k)] = v
}

return lowerHeaders, nil
}

// NewNoopAudit should be used to create a NoopAudit as it handles creation of a
// predictable salt and wraps eventlogger nodes so information can be retrieved on
// what they've seen or formatted.
func NewNoopAudit(config *BackendConfig) (*NoopAudit, error) {
view := &logical.InmemStorage{}

// Create the salt with a known key for predictable hmac values.
se := &logical.StorageEntry{Key: "salt", Value: []byte("foo")}
err := view.Put(context.Background(), se)
if err != nil {
return nil, err
}

// Override the salt related config settings.
backendConfig := &BackendConfig{
SaltView: view,
SaltConfig: &salt.Config{
HMAC: sha256.New,
HMACType: "hmac-sha256",
},
Config: config.Config,
MountPath: config.MountPath,
}

noopBackend := &NoopAudit{
Config: backendConfig,
nodeIDList: make([]eventlogger.NodeID, 2),
nodeMap: make(map[eventlogger.NodeID]eventlogger.Node, 2),
}

cfg, err := NewFormatterConfig(&NoopHeaderFormatter{})
if err != nil {
return nil, err
}

formatterNodeID, err := event.GenerateNodeID()
if err != nil {
return nil, fmt.Errorf("error generating random NodeID for formatter node: %w", err)
}

formatterNode, err := NewEntryFormatter(config.MountPath, cfg, noopBackend, config.Logger)
if err != nil {
return nil, fmt.Errorf("error creating formatter: %w", err)
}

// Wrap the formatting node, so we can get any bytes that were formatted etc.
wrappedFormatter := &noopWrapper{format: "json", node: formatterNode, backend: noopBackend}

noopBackend.nodeIDList[0] = formatterNodeID
noopBackend.nodeMap[formatterNodeID] = wrappedFormatter

sinkNode := event.NewNoopSink()
sinkNodeID, err := event.GenerateNodeID()
if err != nil {
return nil, fmt.Errorf("error generating random NodeID for sink node: %w", err)
}

noopBackend.nodeIDList[1] = sinkNodeID
noopBackend.nodeMap[sinkNodeID] = sinkNode

return noopBackend, nil
}

// NoopAuditFactory should be used when the test needs a way to access bytes that
// have been formatted by the pipeline during audit requests.
// The records parameter will be repointed to the one used within the pipeline.
func NoopAuditFactory(records **[][]byte) Factory {
return func(config *BackendConfig, _ HeaderFormatter) (Backend, error) {
n, err := NewNoopAudit(config)
if err != nil {
return nil, err
}
if records != nil {
*records = &n.records
}

return n, nil
}
}

// Process handles the contortions required by older test code to ensure behavior.
// It will attempt to do some pre/post processing of the logical.LogInput that should
// form part of the event's payload data, as well as capturing the resulting headers
// that were formatted and track the overall bytes that a formatted event uses when
// it's ready to head down the pipeline to the sink node (a noop for us).
func (n *noopWrapper) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) {
n.backend.l.Lock()
defer n.backend.l.Unlock()

var err error

// We're expecting audit events since this is an audit device.
a, ok := e.Payload.(*AuditEvent)
if !ok {
return nil, errors.New("cannot parse payload as an audit event")
}

if n.backend.listener != nil {
n.backend.listener(a)
}

in := a.Data

// Depending on the type of the audit event (request or response) we need to
// track different things.
switch a.Subtype {
case RequestType:
n.backend.ReqAuth = append(n.backend.ReqAuth, in.Auth)
n.backend.Req = append(n.backend.Req, in.Request)
n.backend.ReqNonHMACKeys = in.NonHMACReqDataKeys
n.backend.ReqErrs = append(n.backend.ReqErrs, in.OuterErr)

if n.backend.ReqErr != nil {
return nil, n.backend.ReqErr
}
case ResponseType:
n.backend.RespAuth = append(n.backend.RespAuth, in.Auth)
n.backend.RespReq = append(n.backend.RespReq, in.Request)
n.backend.Resp = append(n.backend.Resp, in.Response)
n.backend.RespErrs = append(n.backend.RespErrs, in.OuterErr)

if in.Response != nil {
n.backend.RespNonHMACKeys = append(n.backend.RespNonHMACKeys, in.NonHMACRespDataKeys)
n.backend.RespReqNonHMACKeys = append(n.backend.RespReqNonHMACKeys, in.NonHMACReqDataKeys)
}

if n.backend.RespErr != nil {
return nil, n.backend.RespErr
}
default:
return nil, fmt.Errorf("unknown audit event type: %q", a.Subtype)
}

// Once we've taken note of the relevant properties of the event, we get the
// underlying (wrapped) node to process it as normal.
e, err = n.node.Process(ctx, e)
if err != nil {
return nil, fmt.Errorf("error processing wrapped node: %w", err)
}

// Once processing has been carried out, the underlying node (a formatter node)
// should contain the output ready for the sink node. We'll get that in order
// to track how many bytes we formatted.
b, ok := e.Format(n.format)
if ok {
n.backend.records = append(n.backend.records, b)
}

// Finally, the last bit of post-processing is to make sure that we track the
// formatted headers that would have made it to the logs via the sink node.
// They only appear in requests.
if a.Subtype == RequestType {
reqEntry := &RequestEntry{}
err = json.Unmarshal(b, &reqEntry)
if err != nil {
return nil, fmt.Errorf("unable to parse formatted audit entry data: %w", err)
}

n.backend.ReqHeaders = append(n.backend.ReqHeaders, reqEntry.Request.Headers)
}

// Return the event and no error in order to let the pipeline continue on.
return e, nil
}

func (n *noopWrapper) Reopen() error {
return n.node.Reopen()
}

func (n *noopWrapper) Type() eventlogger.NodeType {
return n.node.Type()
}

// LogTestMessage will manually crank the handle on the nodes associated with this backend.
func (n *NoopAudit) LogTestMessage(ctx context.Context, in *logical.LogInput) error {
if len(n.nodeIDList) > 0 {
return ProcessManual(ctx, in, n.nodeIDList, n.nodeMap)
}

return nil
}

func (n *NoopAudit) Salt(ctx context.Context) (*salt.Salt, error) {
n.saltMutex.RLock()
if n.salt != nil {
defer n.saltMutex.RUnlock()
return n.salt, nil
}
n.saltMutex.RUnlock()
n.saltMutex.Lock()
defer n.saltMutex.Unlock()
if n.salt != nil {
return n.salt, nil
}
s, err := salt.NewSalt(ctx, n.Config.SaltView, n.Config.SaltConfig)
if err != nil {
return nil, err
}
n.salt = s
return s, nil
}

func (n *NoopAudit) GetHash(ctx context.Context, data string) (string, error) {
s, err := n.Salt(ctx)
if err != nil {
return "", err
}
return s.GetIdentifiedHMAC(data), nil
}

func (n *NoopAudit) Reload() error {
return nil
}

func (n *NoopAudit) Invalidate(_ context.Context) {
n.saltMutex.Lock()
defer n.saltMutex.Unlock()
n.salt = nil
}

func (n *NoopAudit) EventType() eventlogger.EventType {
return event.AuditType.AsEventType()
}

func (n *NoopAudit) HasFiltering() bool {
return false
}

func (n *NoopAudit) Name() string {
return n.Config.MountPath
}

func (n *NoopAudit) Nodes() map[eventlogger.NodeID]eventlogger.Node {
return n.nodeMap
}

func (n *NoopAudit) NodeIDs() []eventlogger.NodeID {
return n.nodeIDList
}

func (n *NoopAudit) IsFallback() bool {
return false
}

func TestNoopAudit(t *testing.T, path string, config map[string]string) *NoopAudit {
cfg := &BackendConfig{
Config: config,
MountPath: path,
Logger: corehelpers.NewTestLogger(t),
}
n, err := NewNoopAudit(cfg)
if err != nil {
t.Fatal(err)
}
return n
}
4 changes: 2 additions & 2 deletions audit/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ type Backend interface {
LogTestMessage(context.Context, *logical.LogInput) error

// Reload is called on SIGHUP for supporting backends.
Reload(context.Context) error
Reload() error

// Invalidate is called for path invalidation
Invalidate(context.Context)
Expand Down Expand Up @@ -203,4 +203,4 @@ type BackendConfig struct {
}

// Factory is the factory function to create an audit backend.
type Factory func(context.Context, *BackendConfig, HeaderFormatter) (Backend, error)
type Factory func(*BackendConfig, HeaderFormatter) (Backend, error)
4 changes: 2 additions & 2 deletions builtin/audit/file/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ type Backend struct {
saltView logical.Storage
}

func Factory(_ context.Context, conf *audit.BackendConfig, headersConfig audit.HeaderFormatter) (audit.Backend, error) {
func Factory(conf *audit.BackendConfig, headersConfig audit.HeaderFormatter) (audit.Backend, error) {
if conf.SaltConfig == nil {
return nil, fmt.Errorf("nil salt config: %w", audit.ErrInvalidParameter)
}
Expand Down Expand Up @@ -164,7 +164,7 @@ func (b *Backend) LogTestMessage(ctx context.Context, in *logical.LogInput) erro
return nil
}

func (b *Backend) Reload(_ context.Context) error {
func (b *Backend) Reload() error {
for _, n := range b.nodeMap {
if n.Type() == eventlogger.NodeTypeSink {
return n.Reopen()
Expand Down
Loading

0 comments on commit 38a7869

Please sign in to comment.