-
Notifications
You must be signed in to change notification settings - Fork 4.3k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
move noop audit to audit package (#26448)
- Loading branch information
Peter Wilson
authored
Apr 16, 2024
1 parent
76be7fb
commit 38a7869
Showing
25 changed files
with
435 additions
and
468 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.