mirror of
https://github.com/optim-enterprises-bv/vault.git
synced 2025-10-30 02:02:43 +00:00
VAULT-18284: Audit refactor packages (#21972)
* initial git mv to rename 'audit' packages * remove 'Audit' prefix from structs inside audit package * refactor of event/audit pacakges * EventFormatter => EntryFormatter * 'AuditFormat' => EntryFormat * Use NewFormatterConfig func --------- Co-authored-by: Marc Boudreau <marc.boudreau@hashicorp.com>
This commit is contained in:
@@ -1,62 +0,0 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package audit
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
|
|
||||||
"github.com/hashicorp/vault/sdk/helper/salt"
|
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Backend interface must be implemented for an audit
|
|
||||||
// mechanism to be made available. Audit backends can be enabled to
|
|
||||||
// sink information to different backends such as logs, file, databases,
|
|
||||||
// or other external services.
|
|
||||||
type Backend interface {
|
|
||||||
// LogRequest is used to synchronously log a request. This is done after the
|
|
||||||
// request is authorized but before the request is executed. The arguments
|
|
||||||
// MUST not be modified in anyway. They should be deep copied if this is
|
|
||||||
// a possibility.
|
|
||||||
LogRequest(context.Context, *logical.LogInput) error
|
|
||||||
|
|
||||||
// LogResponse is used to synchronously log a response. This is done after
|
|
||||||
// the request is processed but before the response is sent. The arguments
|
|
||||||
// MUST not be modified in anyway. They should be deep copied if this is
|
|
||||||
// a possibility.
|
|
||||||
LogResponse(context.Context, *logical.LogInput) error
|
|
||||||
|
|
||||||
// LogTestMessage is used to check an audit backend before adding it
|
|
||||||
// permanently. It should attempt to synchronously log the given test
|
|
||||||
// message, WITHOUT using the normal Salt (which would require a storage
|
|
||||||
// operation on creation, which is currently disallowed.)
|
|
||||||
LogTestMessage(context.Context, *logical.LogInput, map[string]string) error
|
|
||||||
|
|
||||||
// GetHash is used to return the given data with the backend's hash,
|
|
||||||
// so that a caller can determine if a value in the audit log matches
|
|
||||||
// an expected plaintext value
|
|
||||||
GetHash(context.Context, string) (string, error)
|
|
||||||
|
|
||||||
// Reload is called on SIGHUP for supporting backends.
|
|
||||||
Reload(context.Context) error
|
|
||||||
|
|
||||||
// Invalidate is called for path invalidation
|
|
||||||
Invalidate(context.Context)
|
|
||||||
}
|
|
||||||
|
|
||||||
// BackendConfig contains configuration parameters used in the factory func to
|
|
||||||
// instantiate audit backends
|
|
||||||
type BackendConfig struct {
|
|
||||||
// The view to store the salt
|
|
||||||
SaltView logical.Storage
|
|
||||||
|
|
||||||
// The salt config that should be used for any secret obfuscation
|
|
||||||
SaltConfig *salt.Config
|
|
||||||
|
|
||||||
// Config is the opaque user configuration provided when mounting
|
|
||||||
Config map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Factory is the factory function to create an audit backend.
|
|
||||||
type Factory func(context.Context, *BackendConfig, bool) (Backend, error)
|
|
||||||
571
audit/entry_formatter.go
Normal file
571
audit/entry_formatter.go
Normal file
@@ -0,0 +1,571 @@
|
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package audit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/jefferai/jsonx"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/helper/namespace"
|
||||||
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
|
|
||||||
|
"github.com/go-jose/go-jose/v3/jwt"
|
||||||
|
"github.com/hashicorp/vault/internal/observability/event"
|
||||||
|
"github.com/hashicorp/vault/sdk/helper/jsonutil"
|
||||||
|
|
||||||
|
"github.com/hashicorp/eventlogger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ Formatter = (*EntryFormatter)(nil)
|
||||||
|
_ eventlogger.Node = (*EntryFormatter)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewEntryFormatter should be used to create an EntryFormatter.
|
||||||
|
// Accepted options: WithPrefix.
|
||||||
|
func NewEntryFormatter(config FormatterConfig, salter Salter, opt ...Option) (*EntryFormatter, error) {
|
||||||
|
const op = "audit.NewEntryFormatter"
|
||||||
|
|
||||||
|
if salter == nil {
|
||||||
|
return nil, fmt.Errorf("%s: cannot create a new audit formatter with nil salter: %w", op, event.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)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := getOpts(opt...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: error applying options: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &EntryFormatter{
|
||||||
|
salter: salter,
|
||||||
|
config: config,
|
||||||
|
prefix: opts.withPrefix,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reopen is a no-op for the formatter node.
|
||||||
|
func (_ *EntryFormatter) Reopen() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type describes the type of this node (formatter).
|
||||||
|
func (_ *EntryFormatter) Type() eventlogger.NodeType {
|
||||||
|
return eventlogger.NodeTypeFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func (f *EntryFormatter) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) {
|
||||||
|
const op = "audit.(EntryFormatter).Process"
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if e == nil {
|
||||||
|
return nil, fmt.Errorf("%s: event is nil: %w", op, event.ErrInvalidParameter)
|
||||||
|
}
|
||||||
|
|
||||||
|
a, ok := e.Payload.(*auditEvent)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%s: cannot parse event payload: %w", op, event.ErrInvalidParameter)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result []byte
|
||||||
|
|
||||||
|
switch a.Subtype {
|
||||||
|
case RequestType:
|
||||||
|
entry, err := f.FormatRequest(ctx, a.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: unable to parse request from audit event: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err = jsonutil.EncodeJSON(entry)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: unable to format request: %w", op, err)
|
||||||
|
}
|
||||||
|
case ResponseType:
|
||||||
|
entry, err := f.FormatResponse(ctx, a.Data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: unable to parse response from audit event: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err = jsonutil.EncodeJSON(entry)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: unable to format response: %w", op, err)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("%s: unknown audit event subtype: %q", op, a.Subtype)
|
||||||
|
}
|
||||||
|
|
||||||
|
if f.config.RequiredFormat == JSONxFormat {
|
||||||
|
var err error
|
||||||
|
result, err = jsonx.EncodeJSONBytes(result)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: unable to encode JSONx using JSON data: %w", op, err)
|
||||||
|
}
|
||||||
|
if result == nil {
|
||||||
|
return nil, fmt.Errorf("%s: encoded JSONx was nil: %w", op, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// This makes a bit of a mess of the 'format' since both JSON and XML (JSONx)
|
||||||
|
// don't support a prefix just sitting there.
|
||||||
|
// However, this would be a breaking change to how Vault currently works to
|
||||||
|
// include the prefix as part of the JSON object or XML document.
|
||||||
|
if f.prefix != "" {
|
||||||
|
result = append([]byte(f.prefix), result...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Store the final format.
|
||||||
|
e.FormattedAs(f.config.RequiredFormat.String(), result)
|
||||||
|
|
||||||
|
return e, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatRequest attempts to format the specified logical.LogInput into a RequestEntry.
|
||||||
|
func (f *EntryFormatter) FormatRequest(ctx context.Context, in *logical.LogInput) (*RequestEntry, error) {
|
||||||
|
switch {
|
||||||
|
case in == nil || in.Request == nil:
|
||||||
|
return nil, errors.New("request to request-audit a nil request")
|
||||||
|
case f.salter == nil:
|
||||||
|
return nil, errors.New("salt func not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := f.salter.Salt(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error fetching salt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set these to the input values at first
|
||||||
|
auth := in.Auth
|
||||||
|
req := in.Request
|
||||||
|
var connState *tls.ConnectionState
|
||||||
|
if auth == nil {
|
||||||
|
auth = new(logical.Auth)
|
||||||
|
}
|
||||||
|
|
||||||
|
if in.Request.Connection != nil && in.Request.Connection.ConnState != nil {
|
||||||
|
connState = in.Request.Connection.ConnState
|
||||||
|
}
|
||||||
|
|
||||||
|
if !f.config.Raw {
|
||||||
|
auth, err = HashAuth(s, auth, f.config.HMACAccessor)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err = HashRequest(s, req, f.config.HMACAccessor, in.NonHMACReqDataKeys)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var errString string
|
||||||
|
if in.OuterErr != nil {
|
||||||
|
errString = in.OuterErr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
ns, err := namespace.FromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
reqType := in.Type
|
||||||
|
if reqType == "" {
|
||||||
|
reqType = "request"
|
||||||
|
}
|
||||||
|
reqEntry := &RequestEntry{
|
||||||
|
Type: reqType,
|
||||||
|
Error: errString,
|
||||||
|
ForwardedFrom: req.ForwardedFrom,
|
||||||
|
Auth: &Auth{
|
||||||
|
ClientToken: auth.ClientToken,
|
||||||
|
Accessor: auth.Accessor,
|
||||||
|
DisplayName: auth.DisplayName,
|
||||||
|
Policies: auth.Policies,
|
||||||
|
TokenPolicies: auth.TokenPolicies,
|
||||||
|
IdentityPolicies: auth.IdentityPolicies,
|
||||||
|
ExternalNamespacePolicies: auth.ExternalNamespacePolicies,
|
||||||
|
NoDefaultPolicy: auth.NoDefaultPolicy,
|
||||||
|
Metadata: auth.Metadata,
|
||||||
|
EntityID: auth.EntityID,
|
||||||
|
RemainingUses: req.ClientTokenRemainingUses,
|
||||||
|
TokenType: auth.TokenType.String(),
|
||||||
|
TokenTTL: int64(auth.TTL.Seconds()),
|
||||||
|
},
|
||||||
|
|
||||||
|
Request: &Request{
|
||||||
|
ID: req.ID,
|
||||||
|
ClientID: req.ClientID,
|
||||||
|
ClientToken: req.ClientToken,
|
||||||
|
ClientTokenAccessor: req.ClientTokenAccessor,
|
||||||
|
Operation: req.Operation,
|
||||||
|
MountPoint: req.MountPoint,
|
||||||
|
MountType: req.MountType,
|
||||||
|
MountAccessor: req.MountAccessor,
|
||||||
|
MountRunningVersion: req.MountRunningVersion(),
|
||||||
|
MountRunningSha256: req.MountRunningSha256(),
|
||||||
|
MountIsExternalPlugin: req.MountIsExternalPlugin(),
|
||||||
|
MountClass: req.MountClass(),
|
||||||
|
Namespace: &Namespace{
|
||||||
|
ID: ns.ID,
|
||||||
|
Path: ns.Path,
|
||||||
|
},
|
||||||
|
Path: req.Path,
|
||||||
|
Data: req.Data,
|
||||||
|
PolicyOverride: req.PolicyOverride,
|
||||||
|
RemoteAddr: getRemoteAddr(req),
|
||||||
|
RemotePort: getRemotePort(req),
|
||||||
|
ReplicationCluster: req.ReplicationCluster,
|
||||||
|
Headers: req.Headers,
|
||||||
|
ClientCertificateSerialNumber: getClientCertificateSerialNumber(connState),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !auth.IssueTime.IsZero() {
|
||||||
|
reqEntry.Auth.TokenIssueTime = auth.IssueTime.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth.PolicyResults != nil {
|
||||||
|
reqEntry.Auth.PolicyResults = &PolicyResults{
|
||||||
|
Allowed: auth.PolicyResults.Allowed,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range auth.PolicyResults.GrantingPolicies {
|
||||||
|
reqEntry.Auth.PolicyResults.GrantingPolicies = append(reqEntry.Auth.PolicyResults.GrantingPolicies, PolicyInfo{
|
||||||
|
Name: p.Name,
|
||||||
|
NamespaceId: p.NamespaceId,
|
||||||
|
NamespacePath: p.NamespacePath,
|
||||||
|
Type: p.Type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.WrapInfo != nil {
|
||||||
|
reqEntry.Request.WrapTTL = int(req.WrapInfo.TTL / time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !f.config.OmitTime {
|
||||||
|
reqEntry.Time = time.Now().UTC().Format(time.RFC3339Nano)
|
||||||
|
}
|
||||||
|
|
||||||
|
return reqEntry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatResponse attempts to format the specified logical.LogInput into a ResponseEntry.
|
||||||
|
func (f *EntryFormatter) FormatResponse(ctx context.Context, in *logical.LogInput) (*ResponseEntry, error) {
|
||||||
|
switch {
|
||||||
|
case f == nil:
|
||||||
|
return nil, errors.New("formatter is nil")
|
||||||
|
case in == nil || in.Request == nil:
|
||||||
|
return nil, errors.New("request to response-audit a nil request")
|
||||||
|
case f.salter == nil:
|
||||||
|
return nil, errors.New("salt func not configured")
|
||||||
|
}
|
||||||
|
|
||||||
|
s, err := f.salter.Salt(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("error fetching salt: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set these to the input values at first
|
||||||
|
auth, req, resp := in.Auth, in.Request, in.Response
|
||||||
|
if auth == nil {
|
||||||
|
auth = new(logical.Auth)
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
resp = new(logical.Response)
|
||||||
|
}
|
||||||
|
var connState *tls.ConnectionState
|
||||||
|
|
||||||
|
if in.Request.Connection != nil && in.Request.Connection.ConnState != nil {
|
||||||
|
connState = in.Request.Connection.ConnState
|
||||||
|
}
|
||||||
|
|
||||||
|
elideListResponseData := f.config.ElideListResponses && req.Operation == logical.ListOperation
|
||||||
|
|
||||||
|
var respData map[string]interface{}
|
||||||
|
if f.config.Raw {
|
||||||
|
// In the non-raw case, elision of list response data occurs inside HashResponse, to avoid redundant deep
|
||||||
|
// copies and hashing of data only to elide it later. In the raw case, we need to do it here.
|
||||||
|
if elideListResponseData && resp.Data != nil {
|
||||||
|
// Copy the data map before making changes, but we only need to go one level deep in this case
|
||||||
|
respData = make(map[string]interface{}, len(resp.Data))
|
||||||
|
for k, v := range resp.Data {
|
||||||
|
respData[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
doElideListResponseData(respData)
|
||||||
|
} else {
|
||||||
|
respData = resp.Data
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
auth, err = HashAuth(s, auth, f.config.HMACAccessor)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req, err = HashRequest(s, req, f.config.HMACAccessor, in.NonHMACReqDataKeys)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err = HashResponse(s, resp, f.config.HMACAccessor, in.NonHMACRespDataKeys, elideListResponseData)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
respData = resp.Data
|
||||||
|
}
|
||||||
|
|
||||||
|
var errString string
|
||||||
|
if in.OuterErr != nil {
|
||||||
|
errString = in.OuterErr.Error()
|
||||||
|
}
|
||||||
|
|
||||||
|
ns, err := namespace.FromContext(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var respAuth *Auth
|
||||||
|
if resp.Auth != nil {
|
||||||
|
respAuth = &Auth{
|
||||||
|
ClientToken: resp.Auth.ClientToken,
|
||||||
|
Accessor: resp.Auth.Accessor,
|
||||||
|
DisplayName: resp.Auth.DisplayName,
|
||||||
|
Policies: resp.Auth.Policies,
|
||||||
|
TokenPolicies: resp.Auth.TokenPolicies,
|
||||||
|
IdentityPolicies: resp.Auth.IdentityPolicies,
|
||||||
|
ExternalNamespacePolicies: resp.Auth.ExternalNamespacePolicies,
|
||||||
|
NoDefaultPolicy: resp.Auth.NoDefaultPolicy,
|
||||||
|
Metadata: resp.Auth.Metadata,
|
||||||
|
NumUses: resp.Auth.NumUses,
|
||||||
|
EntityID: resp.Auth.EntityID,
|
||||||
|
TokenType: resp.Auth.TokenType.String(),
|
||||||
|
TokenTTL: int64(resp.Auth.TTL.Seconds()),
|
||||||
|
}
|
||||||
|
if !resp.Auth.IssueTime.IsZero() {
|
||||||
|
respAuth.TokenIssueTime = resp.Auth.IssueTime.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var respSecret *Secret
|
||||||
|
if resp.Secret != nil {
|
||||||
|
respSecret = &Secret{
|
||||||
|
LeaseID: resp.Secret.LeaseID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var respWrapInfo *ResponseWrapInfo
|
||||||
|
if resp.WrapInfo != nil {
|
||||||
|
token := resp.WrapInfo.Token
|
||||||
|
if jwtToken := parseVaultTokenFromJWT(token); jwtToken != nil {
|
||||||
|
token = *jwtToken
|
||||||
|
}
|
||||||
|
respWrapInfo = &ResponseWrapInfo{
|
||||||
|
TTL: int(resp.WrapInfo.TTL / time.Second),
|
||||||
|
Token: token,
|
||||||
|
Accessor: resp.WrapInfo.Accessor,
|
||||||
|
CreationTime: resp.WrapInfo.CreationTime.UTC().Format(time.RFC3339Nano),
|
||||||
|
CreationPath: resp.WrapInfo.CreationPath,
|
||||||
|
WrappedAccessor: resp.WrapInfo.WrappedAccessor,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
respType := in.Type
|
||||||
|
if respType == "" {
|
||||||
|
respType = "response"
|
||||||
|
}
|
||||||
|
respEntry := &ResponseEntry{
|
||||||
|
Type: respType,
|
||||||
|
Error: errString,
|
||||||
|
Forwarded: req.ForwardedFrom != "",
|
||||||
|
Auth: &Auth{
|
||||||
|
ClientToken: auth.ClientToken,
|
||||||
|
Accessor: auth.Accessor,
|
||||||
|
DisplayName: auth.DisplayName,
|
||||||
|
Policies: auth.Policies,
|
||||||
|
TokenPolicies: auth.TokenPolicies,
|
||||||
|
IdentityPolicies: auth.IdentityPolicies,
|
||||||
|
ExternalNamespacePolicies: auth.ExternalNamespacePolicies,
|
||||||
|
NoDefaultPolicy: auth.NoDefaultPolicy,
|
||||||
|
Metadata: auth.Metadata,
|
||||||
|
RemainingUses: req.ClientTokenRemainingUses,
|
||||||
|
EntityID: auth.EntityID,
|
||||||
|
EntityCreated: auth.EntityCreated,
|
||||||
|
TokenType: auth.TokenType.String(),
|
||||||
|
TokenTTL: int64(auth.TTL.Seconds()),
|
||||||
|
},
|
||||||
|
|
||||||
|
Request: &Request{
|
||||||
|
ID: req.ID,
|
||||||
|
ClientToken: req.ClientToken,
|
||||||
|
ClientTokenAccessor: req.ClientTokenAccessor,
|
||||||
|
ClientID: req.ClientID,
|
||||||
|
Operation: req.Operation,
|
||||||
|
MountPoint: req.MountPoint,
|
||||||
|
MountType: req.MountType,
|
||||||
|
MountAccessor: req.MountAccessor,
|
||||||
|
MountRunningVersion: req.MountRunningVersion(),
|
||||||
|
MountRunningSha256: req.MountRunningSha256(),
|
||||||
|
MountIsExternalPlugin: req.MountIsExternalPlugin(),
|
||||||
|
MountClass: req.MountClass(),
|
||||||
|
Namespace: &Namespace{
|
||||||
|
ID: ns.ID,
|
||||||
|
Path: ns.Path,
|
||||||
|
},
|
||||||
|
Path: req.Path,
|
||||||
|
Data: req.Data,
|
||||||
|
PolicyOverride: req.PolicyOverride,
|
||||||
|
RemoteAddr: getRemoteAddr(req),
|
||||||
|
RemotePort: getRemotePort(req),
|
||||||
|
ClientCertificateSerialNumber: getClientCertificateSerialNumber(connState),
|
||||||
|
ReplicationCluster: req.ReplicationCluster,
|
||||||
|
Headers: req.Headers,
|
||||||
|
},
|
||||||
|
|
||||||
|
Response: &Response{
|
||||||
|
MountPoint: req.MountPoint,
|
||||||
|
MountType: req.MountType,
|
||||||
|
MountAccessor: req.MountAccessor,
|
||||||
|
MountRunningVersion: req.MountRunningVersion(),
|
||||||
|
MountRunningSha256: req.MountRunningSha256(),
|
||||||
|
MountIsExternalPlugin: req.MountIsExternalPlugin(),
|
||||||
|
MountClass: req.MountClass(),
|
||||||
|
Auth: respAuth,
|
||||||
|
Secret: respSecret,
|
||||||
|
Data: respData,
|
||||||
|
Warnings: resp.Warnings,
|
||||||
|
Redirect: resp.Redirect,
|
||||||
|
WrapInfo: respWrapInfo,
|
||||||
|
Headers: resp.Headers,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if auth.PolicyResults != nil {
|
||||||
|
respEntry.Auth.PolicyResults = &PolicyResults{
|
||||||
|
Allowed: auth.PolicyResults.Allowed,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range auth.PolicyResults.GrantingPolicies {
|
||||||
|
respEntry.Auth.PolicyResults.GrantingPolicies = append(respEntry.Auth.PolicyResults.GrantingPolicies, PolicyInfo{
|
||||||
|
Name: p.Name,
|
||||||
|
NamespaceId: p.NamespaceId,
|
||||||
|
NamespacePath: p.NamespacePath,
|
||||||
|
Type: p.Type,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !auth.IssueTime.IsZero() {
|
||||||
|
respEntry.Auth.TokenIssueTime = auth.IssueTime.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
if req.WrapInfo != nil {
|
||||||
|
respEntry.Request.WrapTTL = int(req.WrapInfo.TTL / time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !f.config.OmitTime {
|
||||||
|
respEntry.Time = time.Now().UTC().Format(time.RFC3339Nano)
|
||||||
|
}
|
||||||
|
|
||||||
|
return respEntry, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFormatterConfig should be used to create a FormatterConfig.
|
||||||
|
// Accepted options: WithElision, WithHMACAccessor, WithOmitTime, WithRaw, WithFormat.
|
||||||
|
func NewFormatterConfig(opt ...Option) (FormatterConfig, error) {
|
||||||
|
const op = "audit.NewFormatterConfig"
|
||||||
|
|
||||||
|
opts, err := getOpts(opt...)
|
||||||
|
if err != nil {
|
||||||
|
return FormatterConfig{}, fmt.Errorf("%s: error applying options: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return FormatterConfig{
|
||||||
|
ElideListResponses: opts.withElision,
|
||||||
|
HMACAccessor: opts.withHMACAccessor,
|
||||||
|
OmitTime: opts.withOmitTime,
|
||||||
|
Raw: opts.withRaw,
|
||||||
|
RequiredFormat: opts.withFormat,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRemoteAddr safely gets the remote address avoiding a nil pointer
|
||||||
|
func getRemoteAddr(req *logical.Request) string {
|
||||||
|
if req != nil && req.Connection != nil {
|
||||||
|
return req.Connection.RemoteAddr
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// getRemotePort safely gets the remote port avoiding a nil pointer
|
||||||
|
func getRemotePort(req *logical.Request) int {
|
||||||
|
if req != nil && req.Connection != nil {
|
||||||
|
return req.Connection.RemotePort
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// getClientCertificateSerialNumber attempts the retrieve the serial number of
|
||||||
|
// the peer certificate from the specified tls.ConnectionState.
|
||||||
|
func getClientCertificateSerialNumber(connState *tls.ConnectionState) string {
|
||||||
|
if connState == nil || len(connState.VerifiedChains) == 0 || len(connState.VerifiedChains[0]) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return connState.VerifiedChains[0][0].SerialNumber.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseVaultTokenFromJWT returns a string iff the token was a JWT, and we could
|
||||||
|
// extract the original token ID from inside
|
||||||
|
func parseVaultTokenFromJWT(token string) *string {
|
||||||
|
if strings.Count(token, ".") != 2 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parsedJWT, err := jwt.ParseSigned(token)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var claims jwt.Claims
|
||||||
|
if err = parsedJWT.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return &claims.ID
|
||||||
|
}
|
||||||
|
|
||||||
|
// doElideListResponseData performs the actual elision of list operation response data, once surrounding code has
|
||||||
|
// determined it should apply to a particular request. The data map that is passed in must be a copy that is safe to
|
||||||
|
// modify in place, but need not be a full recursive deep copy, as only top-level keys are changed.
|
||||||
|
//
|
||||||
|
// See the documentation of the controlling option in FormatterConfig for more information on the purpose.
|
||||||
|
func doElideListResponseData(data map[string]interface{}) {
|
||||||
|
for k, v := range data {
|
||||||
|
if k == "keys" {
|
||||||
|
if vSlice, ok := v.([]string); ok {
|
||||||
|
data[k] = len(vSlice)
|
||||||
|
}
|
||||||
|
} else if k == "key_info" {
|
||||||
|
if vMap, ok := v.(map[string]interface{}); ok {
|
||||||
|
data[k] = len(vMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
401
audit/entry_formatter_test.go
Normal file
401
audit/entry_formatter_test.go
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package audit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/internal/observability/event"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/helper/namespace"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
|
|
||||||
|
"github.com/hashicorp/eventlogger"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// fakeEvent will return a new fake event containing audit data based on the
|
||||||
|
// specified subtype, format and logical.LogInput.
|
||||||
|
func fakeEvent(tb testing.TB, subtype subtype, format format, input *logical.LogInput) *eventlogger.Event {
|
||||||
|
tb.Helper()
|
||||||
|
|
||||||
|
date := time.Date(2023, time.July, 11, 15, 49, 10, 0o0, time.Local)
|
||||||
|
|
||||||
|
auditEvent, err := newEvent(subtype, format,
|
||||||
|
WithID("123"),
|
||||||
|
WithNow(date),
|
||||||
|
)
|
||||||
|
require.NoError(tb, err)
|
||||||
|
require.NotNil(tb, auditEvent)
|
||||||
|
require.Equal(tb, "123", auditEvent.ID)
|
||||||
|
require.Equal(tb, "v0.1", auditEvent.Version)
|
||||||
|
require.Equal(tb, format, auditEvent.RequiredFormat)
|
||||||
|
require.Equal(tb, subtype, auditEvent.Subtype)
|
||||||
|
require.Equal(tb, date, auditEvent.Timestamp)
|
||||||
|
|
||||||
|
auditEvent.Data = input
|
||||||
|
|
||||||
|
e := &eventlogger.Event{
|
||||||
|
Type: eventlogger.EventType(event.AuditType),
|
||||||
|
CreatedAt: auditEvent.Timestamp,
|
||||||
|
Formatted: make(map[string][]byte),
|
||||||
|
Payload: auditEvent,
|
||||||
|
}
|
||||||
|
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNewEntryFormatter ensures we can create new EntryFormatter structs.
|
||||||
|
func TestNewEntryFormatter(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
UseStaticSalt bool
|
||||||
|
Options []Option // Only supports WithPrefix
|
||||||
|
IsErrorExpected bool
|
||||||
|
ExpectedErrorMessage string
|
||||||
|
ExpectedFormat format
|
||||||
|
ExpectedPrefix string
|
||||||
|
}{
|
||||||
|
"nil-salter": {
|
||||||
|
UseStaticSalt: false,
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "audit.NewEntryFormatter: cannot create a new audit formatter with nil salter: invalid parameter",
|
||||||
|
},
|
||||||
|
"static-salter": {
|
||||||
|
UseStaticSalt: true,
|
||||||
|
IsErrorExpected: false,
|
||||||
|
Options: []Option{
|
||||||
|
WithFormat(JSONFormat.String()),
|
||||||
|
},
|
||||||
|
ExpectedFormat: JSONFormat,
|
||||||
|
},
|
||||||
|
"default": {
|
||||||
|
UseStaticSalt: true,
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedFormat: JSONFormat,
|
||||||
|
},
|
||||||
|
"config-json": {
|
||||||
|
UseStaticSalt: true,
|
||||||
|
Options: []Option{
|
||||||
|
WithFormat(JSONFormat.String()),
|
||||||
|
},
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedFormat: JSONFormat,
|
||||||
|
},
|
||||||
|
"config-jsonx": {
|
||||||
|
UseStaticSalt: true,
|
||||||
|
Options: []Option{
|
||||||
|
WithFormat(JSONxFormat.String()),
|
||||||
|
},
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedFormat: JSONxFormat,
|
||||||
|
},
|
||||||
|
"config-json-prefix": {
|
||||||
|
UseStaticSalt: true,
|
||||||
|
Options: []Option{
|
||||||
|
WithPrefix("foo"),
|
||||||
|
WithFormat(JSONFormat.String()),
|
||||||
|
},
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedFormat: JSONFormat,
|
||||||
|
ExpectedPrefix: "foo",
|
||||||
|
},
|
||||||
|
"config-jsonx-prefix": {
|
||||||
|
UseStaticSalt: true,
|
||||||
|
Options: []Option{
|
||||||
|
WithPrefix("foo"),
|
||||||
|
WithFormat(JSONxFormat.String()),
|
||||||
|
},
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedFormat: JSONxFormat,
|
||||||
|
ExpectedPrefix: "foo",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
var ss Salter
|
||||||
|
if tc.UseStaticSalt {
|
||||||
|
ss = newStaticSalt(t)
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg, err := NewFormatterConfig(tc.Options...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
f, err := NewEntryFormatter(cfg, ss, tc.Options...)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case tc.IsErrorExpected:
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
||||||
|
require.Nil(t, f)
|
||||||
|
default:
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, f)
|
||||||
|
require.Equal(t, tc.ExpectedFormat, f.config.RequiredFormat)
|
||||||
|
require.Equal(t, tc.ExpectedPrefix, f.prefix)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEntryFormatter_Reopen ensures that we do not get an error when calling Reopen.
|
||||||
|
func TestEntryFormatter_Reopen(t *testing.T) {
|
||||||
|
ss := newStaticSalt(t)
|
||||||
|
cfg, err := NewFormatterConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
f, err := NewEntryFormatter(cfg, ss)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, f)
|
||||||
|
require.NoError(t, f.Reopen())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEntryFormatter_Type ensures that the node is a 'formatter' type.
|
||||||
|
func TestEntryFormatter_Type(t *testing.T) {
|
||||||
|
ss := newStaticSalt(t)
|
||||||
|
cfg, err := NewFormatterConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
f, err := NewEntryFormatter(cfg, ss)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, f)
|
||||||
|
require.Equal(t, eventlogger.NodeTypeFormatter, f.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestEntryFormatter_Process attempts to run the Process method to convert the
|
||||||
|
// logical.LogInput within an audit event to JSON and JSONx (RequestEntry or ResponseEntry).
|
||||||
|
func TestEntryFormatter_Process(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
IsErrorExpected bool
|
||||||
|
ExpectedErrorMessage string
|
||||||
|
Subtype subtype
|
||||||
|
RequiredFormat format
|
||||||
|
Data *logical.LogInput
|
||||||
|
RootNamespace bool
|
||||||
|
}{
|
||||||
|
"json-request-no-data": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse request from audit event: request to request-audit a nil request",
|
||||||
|
Subtype: RequestType,
|
||||||
|
RequiredFormat: JSONFormat,
|
||||||
|
Data: nil,
|
||||||
|
},
|
||||||
|
"json-response-no-data": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse response from audit event: request to response-audit a nil request",
|
||||||
|
Subtype: ResponseType,
|
||||||
|
RequiredFormat: JSONFormat,
|
||||||
|
Data: nil,
|
||||||
|
},
|
||||||
|
"json-request-basic-input": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse request from audit event: request to request-audit a nil request",
|
||||||
|
Subtype: RequestType,
|
||||||
|
RequiredFormat: JSONFormat,
|
||||||
|
Data: &logical.LogInput{Type: "magic"},
|
||||||
|
},
|
||||||
|
"json-response-basic-input": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse response from audit event: request to response-audit a nil request",
|
||||||
|
Subtype: ResponseType,
|
||||||
|
RequiredFormat: JSONFormat,
|
||||||
|
Data: &logical.LogInput{Type: "magic"},
|
||||||
|
},
|
||||||
|
"json-request-basic-input-and-request-no-ns": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse request from audit event: no namespace",
|
||||||
|
Subtype: RequestType,
|
||||||
|
RequiredFormat: JSONFormat,
|
||||||
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
||||||
|
},
|
||||||
|
"json-response-basic-input-and-request-no-ns": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse response from audit event: no namespace",
|
||||||
|
Subtype: ResponseType,
|
||||||
|
RequiredFormat: JSONFormat,
|
||||||
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
||||||
|
},
|
||||||
|
"json-request-basic-input-and-request-with-ns": {
|
||||||
|
IsErrorExpected: false,
|
||||||
|
Subtype: RequestType,
|
||||||
|
RequiredFormat: JSONFormat,
|
||||||
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
||||||
|
RootNamespace: true,
|
||||||
|
},
|
||||||
|
"json-response-basic-input-and-request-with-ns": {
|
||||||
|
IsErrorExpected: false,
|
||||||
|
Subtype: ResponseType,
|
||||||
|
RequiredFormat: JSONFormat,
|
||||||
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
||||||
|
RootNamespace: true,
|
||||||
|
},
|
||||||
|
"jsonx-request-no-data": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse request from audit event: request to request-audit a nil request",
|
||||||
|
Subtype: RequestType,
|
||||||
|
RequiredFormat: JSONxFormat,
|
||||||
|
Data: nil,
|
||||||
|
},
|
||||||
|
"jsonx-response-no-data": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse response from audit event: request to response-audit a nil request",
|
||||||
|
Subtype: ResponseType,
|
||||||
|
RequiredFormat: JSONxFormat,
|
||||||
|
Data: nil,
|
||||||
|
},
|
||||||
|
"jsonx-request-basic-input": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse request from audit event: request to request-audit a nil request",
|
||||||
|
Subtype: RequestType,
|
||||||
|
RequiredFormat: JSONxFormat,
|
||||||
|
Data: &logical.LogInput{Type: "magic"},
|
||||||
|
},
|
||||||
|
"jsonx-response-basic-input": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse response from audit event: request to response-audit a nil request",
|
||||||
|
Subtype: ResponseType,
|
||||||
|
RequiredFormat: JSONxFormat,
|
||||||
|
Data: &logical.LogInput{Type: "magic"},
|
||||||
|
},
|
||||||
|
"jsonx-request-basic-input-and-request-no-ns": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse request from audit event: no namespace",
|
||||||
|
Subtype: RequestType,
|
||||||
|
RequiredFormat: JSONxFormat,
|
||||||
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
||||||
|
},
|
||||||
|
"jsonx-response-basic-input-and-request-no-ns": {
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "audit.(EntryFormatter).Process: unable to parse response from audit event: no namespace",
|
||||||
|
Subtype: ResponseType,
|
||||||
|
RequiredFormat: JSONxFormat,
|
||||||
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
||||||
|
},
|
||||||
|
"jsonx-request-basic-input-and-request-with-ns": {
|
||||||
|
IsErrorExpected: false,
|
||||||
|
Subtype: RequestType,
|
||||||
|
RequiredFormat: JSONxFormat,
|
||||||
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
||||||
|
RootNamespace: true,
|
||||||
|
},
|
||||||
|
"jsonx-response-basic-input-and-request-with-ns": {
|
||||||
|
IsErrorExpected: false,
|
||||||
|
Subtype: ResponseType,
|
||||||
|
RequiredFormat: JSONxFormat,
|
||||||
|
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
||||||
|
RootNamespace: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
e := fakeEvent(t, tc.Subtype, tc.RequiredFormat, tc.Data)
|
||||||
|
require.NotNil(t, e)
|
||||||
|
|
||||||
|
ss := newStaticSalt(t)
|
||||||
|
cfg, err := NewFormatterConfig(WithFormat(tc.RequiredFormat.String()))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
f, err := NewEntryFormatter(cfg, ss)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, f)
|
||||||
|
|
||||||
|
var ctx context.Context
|
||||||
|
switch {
|
||||||
|
case tc.RootNamespace:
|
||||||
|
ctx = namespace.RootContext(context.Background())
|
||||||
|
default:
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
|
||||||
|
processed, err := f.Process(ctx, e)
|
||||||
|
b, found := e.Format(string(tc.RequiredFormat))
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case tc.IsErrorExpected:
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
||||||
|
require.Nil(t, processed)
|
||||||
|
require.False(t, found)
|
||||||
|
require.Nil(t, b)
|
||||||
|
default:
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, processed)
|
||||||
|
require.True(t, found)
|
||||||
|
require.NotNil(t, b)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// BenchmarkAuditFileSink_Process benchmarks the EntryFormatter and then event.FileSink calling Process.
|
||||||
|
// This should replicate the original benchmark testing which used to perform both of these roles together.
|
||||||
|
func BenchmarkAuditFileSink_Process(b *testing.B) {
|
||||||
|
// Base input
|
||||||
|
in := &logical.LogInput{
|
||||||
|
Auth: &logical.Auth{
|
||||||
|
ClientToken: "foo",
|
||||||
|
Accessor: "bar",
|
||||||
|
EntityID: "foobarentity",
|
||||||
|
DisplayName: "testtoken",
|
||||||
|
NoDefaultPolicy: true,
|
||||||
|
Policies: []string{"root"},
|
||||||
|
TokenType: logical.TokenTypeService,
|
||||||
|
},
|
||||||
|
Request: &logical.Request{
|
||||||
|
Operation: logical.UpdateOperation,
|
||||||
|
Path: "/foo",
|
||||||
|
Connection: &logical.Connection{
|
||||||
|
RemoteAddr: "127.0.0.1",
|
||||||
|
},
|
||||||
|
WrapInfo: &logical.RequestWrapInfo{
|
||||||
|
TTL: 60 * time.Second,
|
||||||
|
},
|
||||||
|
Headers: map[string][]string{
|
||||||
|
"foo": {"bar"},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := namespace.RootContext(nil)
|
||||||
|
|
||||||
|
// Create the formatter node.
|
||||||
|
cfg, err := NewFormatterConfig()
|
||||||
|
require.NoError(b, err)
|
||||||
|
ss := newStaticSalt(b)
|
||||||
|
formatter, err := NewEntryFormatter(cfg, ss)
|
||||||
|
require.NoError(b, err)
|
||||||
|
require.NotNil(b, formatter)
|
||||||
|
|
||||||
|
// Create the sink node.
|
||||||
|
sink, err := event.NewFileSink("/dev/null", JSONFormat.String())
|
||||||
|
require.NoError(b, err)
|
||||||
|
require.NotNil(b, sink)
|
||||||
|
|
||||||
|
// Generate the event
|
||||||
|
event := fakeEvent(b, RequestType, JSONFormat, in)
|
||||||
|
require.NotNil(b, event)
|
||||||
|
|
||||||
|
b.ResetTimer()
|
||||||
|
b.RunParallel(func(pb *testing.PB) {
|
||||||
|
for pb.Next() {
|
||||||
|
event, err = formatter.Process(ctx, event)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
_, err := sink.Process(ctx, event)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
318
audit/entry_formatter_writer.go
Normal file
318
audit/entry_formatter_writer.go
Normal file
@@ -0,0 +1,318 @@
|
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package audit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/sdk/helper/salt"
|
||||||
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
_ Formatter = (*EntryFormatterWriter)(nil)
|
||||||
|
_ Writer = (*EntryFormatterWriter)(nil)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Salter is an interface that provides a way to obtain a Salt for hashing.
|
||||||
|
type Salter interface {
|
||||||
|
// Salt returns a non-nil salt or an error.
|
||||||
|
Salt(context.Context) (*salt.Salt, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Formatter is an interface that is responsible for formatting a request/response into some format.
|
||||||
|
// It is recommended that you pass data through Hash prior to formatting it.
|
||||||
|
type Formatter interface {
|
||||||
|
// FormatRequest formats the logical.LogInput into an RequestEntry.
|
||||||
|
FormatRequest(context.Context, *logical.LogInput) (*RequestEntry, error)
|
||||||
|
// FormatResponse formats the logical.LogInput into an ResponseEntry.
|
||||||
|
FormatResponse(context.Context, *logical.LogInput) (*ResponseEntry, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Writer is an interface that provides a way to write request and response audit entries.
|
||||||
|
// Formatters write their output to an io.Writer.
|
||||||
|
type Writer interface {
|
||||||
|
// WriteRequest writes the request entry to the writer or returns an error.
|
||||||
|
WriteRequest(io.Writer, *RequestEntry) error
|
||||||
|
// WriteResponse writes the response entry to the writer or returns an error.
|
||||||
|
WriteResponse(io.Writer, *ResponseEntry) error
|
||||||
|
}
|
||||||
|
|
||||||
|
// EntryFormatter should be used to format audit entries.
|
||||||
|
type EntryFormatter struct {
|
||||||
|
salter Salter
|
||||||
|
config FormatterConfig
|
||||||
|
prefix string
|
||||||
|
}
|
||||||
|
|
||||||
|
// EntryFormatterWriter should be used to format and write out audit entries.
|
||||||
|
type EntryFormatterWriter struct {
|
||||||
|
Formatter
|
||||||
|
Writer
|
||||||
|
config FormatterConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 audit entry (supported: JSONFormat and JSONxFormat).
|
||||||
|
RequiredFormat format
|
||||||
|
}
|
||||||
|
|
||||||
|
// nonPersistentSalt is used for obtaining a salt that is
|
||||||
|
type nonPersistentSalt struct{}
|
||||||
|
|
||||||
|
type auditEvent struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
Version string `json:"version"`
|
||||||
|
Subtype subtype `json:"subtype"` // the subtype of the audit event.
|
||||||
|
Timestamp time.Time `json:"timestamp"`
|
||||||
|
Data *logical.LogInput `json:"data"`
|
||||||
|
RequiredFormat format `json:"format"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Salt returns a new salt with default configuration and no storage usage, and no error.
|
||||||
|
func (s *nonPersistentSalt) Salt(_ context.Context) (*salt.Salt, error) {
|
||||||
|
return salt.NewNonpersistentSalt(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEntryFormatterWriter should be used to create a new EntryFormatterWriter.
|
||||||
|
// Deprecated: Please move to using eventlogger.Event via EntryFormatter and a sink.
|
||||||
|
func NewEntryFormatterWriter(config FormatterConfig, formatter Formatter, writer Writer) (*EntryFormatterWriter, error) {
|
||||||
|
switch {
|
||||||
|
case formatter == nil:
|
||||||
|
return nil, errors.New("cannot create a new audit formatter writer with nil formatter")
|
||||||
|
case writer == nil:
|
||||||
|
return nil, errors.New("cannot create a new audit formatter writer with nil formatter")
|
||||||
|
}
|
||||||
|
|
||||||
|
fw := &EntryFormatterWriter{
|
||||||
|
Formatter: formatter,
|
||||||
|
Writer: writer,
|
||||||
|
config: config,
|
||||||
|
}
|
||||||
|
|
||||||
|
return fw, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatAndWriteRequest attempts to format the specified logical.LogInput into an RequestEntry,
|
||||||
|
// and then write the request using the specified io.Writer.
|
||||||
|
// Deprecated: Please move to using eventlogger.Event via EntryFormatter and a sink.
|
||||||
|
func (f *EntryFormatterWriter) FormatAndWriteRequest(ctx context.Context, w io.Writer, in *logical.LogInput) error {
|
||||||
|
switch {
|
||||||
|
case in == nil || in.Request == nil:
|
||||||
|
return fmt.Errorf("request to request-audit a nil request")
|
||||||
|
case w == nil:
|
||||||
|
return fmt.Errorf("writer for audit request is nil")
|
||||||
|
case f.Formatter == nil:
|
||||||
|
return fmt.Errorf("no formatter specifed")
|
||||||
|
case f.Writer == nil:
|
||||||
|
return fmt.Errorf("no writer specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
reqEntry, err := f.Formatter.FormatRequest(ctx, in)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.Writer.WriteRequest(w, reqEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FormatAndWriteResponse attempts to format the specified logical.LogInput into an ResponseEntry,
|
||||||
|
// and then write the response using the specified io.Writer.
|
||||||
|
// Deprecated: Please move to using eventlogger.Event via EntryFormatter and a sink.
|
||||||
|
func (f *EntryFormatterWriter) FormatAndWriteResponse(ctx context.Context, w io.Writer, in *logical.LogInput) error {
|
||||||
|
switch {
|
||||||
|
case in == nil || in.Request == nil:
|
||||||
|
return errors.New("request to response-audit a nil request")
|
||||||
|
case w == nil:
|
||||||
|
return errors.New("writer for audit request is nil")
|
||||||
|
case f.Formatter == nil:
|
||||||
|
return errors.New("no formatter specified")
|
||||||
|
case f.Writer == nil:
|
||||||
|
return errors.New("no writer specified")
|
||||||
|
}
|
||||||
|
|
||||||
|
respEntry, err := f.FormatResponse(ctx, in)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.Writer.WriteResponse(w, respEntry)
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequestEntry is the structure of a request audit log entry in Audit.
|
||||||
|
type RequestEntry struct {
|
||||||
|
Time string `json:"time,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Auth *Auth `json:"auth,omitempty"`
|
||||||
|
Request *Request `json:"request,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
ForwardedFrom string `json:"forwarded_from,omitempty"` // Populated in Enterprise when a request is forwarded
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResponseEntry is the structure of a response audit log entry in Audit.
|
||||||
|
type ResponseEntry struct {
|
||||||
|
Time string `json:"time,omitempty"`
|
||||||
|
Type string `json:"type,omitempty"`
|
||||||
|
Auth *Auth `json:"auth,omitempty"`
|
||||||
|
Request *Request `json:"request,omitempty"`
|
||||||
|
Response *Response `json:"response,omitempty"`
|
||||||
|
Error string `json:"error,omitempty"`
|
||||||
|
Forwarded bool `json:"forwarded,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
ClientID string `json:"client_id,omitempty"`
|
||||||
|
ReplicationCluster string `json:"replication_cluster,omitempty"`
|
||||||
|
Operation logical.Operation `json:"operation,omitempty"`
|
||||||
|
MountPoint string `json:"mount_point,omitempty"`
|
||||||
|
MountType string `json:"mount_type,omitempty"`
|
||||||
|
MountAccessor string `json:"mount_accessor,omitempty"`
|
||||||
|
MountRunningVersion string `json:"mount_running_version,omitempty"`
|
||||||
|
MountRunningSha256 string `json:"mount_running_sha256,omitempty"`
|
||||||
|
MountClass string `json:"mount_class,omitempty"`
|
||||||
|
MountIsExternalPlugin bool `json:"mount_is_external_plugin,omitempty"`
|
||||||
|
ClientToken string `json:"client_token,omitempty"`
|
||||||
|
ClientTokenAccessor string `json:"client_token_accessor,omitempty"`
|
||||||
|
Namespace *Namespace `json:"namespace,omitempty"`
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
Data map[string]interface{} `json:"data,omitempty"`
|
||||||
|
PolicyOverride bool `json:"policy_override,omitempty"`
|
||||||
|
RemoteAddr string `json:"remote_address,omitempty"`
|
||||||
|
RemotePort int `json:"remote_port,omitempty"`
|
||||||
|
WrapTTL int `json:"wrap_ttl,omitempty"`
|
||||||
|
Headers map[string][]string `json:"headers,omitempty"`
|
||||||
|
ClientCertificateSerialNumber string `json:"client_certificate_serial_number,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Auth *Auth `json:"auth,omitempty"`
|
||||||
|
MountPoint string `json:"mount_point,omitempty"`
|
||||||
|
MountType string `json:"mount_type,omitempty"`
|
||||||
|
MountAccessor string `json:"mount_accessor,omitempty"`
|
||||||
|
MountRunningVersion string `json:"mount_running_plugin_version,omitempty"`
|
||||||
|
MountRunningSha256 string `json:"mount_running_sha256,omitempty"`
|
||||||
|
MountClass string `json:"mount_class,omitempty"`
|
||||||
|
MountIsExternalPlugin bool `json:"mount_is_external_plugin,omitempty"`
|
||||||
|
Secret *Secret `json:"secret,omitempty"`
|
||||||
|
Data map[string]interface{} `json:"data,omitempty"`
|
||||||
|
Warnings []string `json:"warnings,omitempty"`
|
||||||
|
Redirect string `json:"redirect,omitempty"`
|
||||||
|
WrapInfo *ResponseWrapInfo `json:"wrap_info,omitempty"`
|
||||||
|
Headers map[string][]string `json:"headers,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Auth struct {
|
||||||
|
ClientToken string `json:"client_token,omitempty"`
|
||||||
|
Accessor string `json:"accessor,omitempty"`
|
||||||
|
DisplayName string `json:"display_name,omitempty"`
|
||||||
|
Policies []string `json:"policies,omitempty"`
|
||||||
|
TokenPolicies []string `json:"token_policies,omitempty"`
|
||||||
|
IdentityPolicies []string `json:"identity_policies,omitempty"`
|
||||||
|
ExternalNamespacePolicies map[string][]string `json:"external_namespace_policies,omitempty"`
|
||||||
|
NoDefaultPolicy bool `json:"no_default_policy,omitempty"`
|
||||||
|
PolicyResults *PolicyResults `json:"policy_results,omitempty"`
|
||||||
|
Metadata map[string]string `json:"metadata,omitempty"`
|
||||||
|
NumUses int `json:"num_uses,omitempty"`
|
||||||
|
RemainingUses int `json:"remaining_uses,omitempty"`
|
||||||
|
EntityID string `json:"entity_id,omitempty"`
|
||||||
|
EntityCreated bool `json:"entity_created,omitempty"`
|
||||||
|
TokenType string `json:"token_type,omitempty"`
|
||||||
|
TokenTTL int64 `json:"token_ttl,omitempty"`
|
||||||
|
TokenIssueTime string `json:"token_issue_time,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyResults struct {
|
||||||
|
Allowed bool `json:"allowed"`
|
||||||
|
GrantingPolicies []PolicyInfo `json:"granting_policies,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PolicyInfo struct {
|
||||||
|
Name string `json:"name,omitempty"`
|
||||||
|
NamespaceId string `json:"namespace_id,omitempty"`
|
||||||
|
NamespacePath string `json:"namespace_path,omitempty"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Secret struct {
|
||||||
|
LeaseID string `json:"lease_id,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponseWrapInfo struct {
|
||||||
|
TTL int `json:"ttl,omitempty"`
|
||||||
|
Token string `json:"token,omitempty"`
|
||||||
|
Accessor string `json:"accessor,omitempty"`
|
||||||
|
CreationTime string `json:"creation_time,omitempty"`
|
||||||
|
CreationPath string `json:"creation_path,omitempty"`
|
||||||
|
WrappedAccessor string `json:"wrapped_accessor,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Namespace struct {
|
||||||
|
ID string `json:"id,omitempty"`
|
||||||
|
Path string `json:"path,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewTemporaryFormatter creates a formatter not backed by a persistent salt
|
||||||
|
func NewTemporaryFormatter(requiredFormat, prefix string) (*EntryFormatterWriter, error) {
|
||||||
|
cfg, err := NewFormatterConfig(WithFormat(requiredFormat))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
eventFormatter, err := NewEntryFormatter(cfg, &nonPersistentSalt{}, WithPrefix(prefix))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var w Writer
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case strings.EqualFold(requiredFormat, JSONxFormat.String()):
|
||||||
|
w = &JSONxWriter{Prefix: prefix}
|
||||||
|
default:
|
||||||
|
w = &JSONWriter{Prefix: prefix}
|
||||||
|
}
|
||||||
|
|
||||||
|
fw, err := NewEntryFormatterWriter(cfg, eventFormatter, w)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fw, nil
|
||||||
|
}
|
||||||
@@ -17,9 +17,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// newStaticSalt returns a new staticSalt for use in testing.
|
// newStaticSalt returns a new staticSalt for use in testing.
|
||||||
func newStaticSalt(t *testing.T) *staticSalt {
|
func newStaticSalt(tb testing.TB) *staticSalt {
|
||||||
s, err := salt.NewSalt(context.Background(), nil, nil)
|
s, err := salt.NewSalt(context.Background(), nil, nil)
|
||||||
require.NoError(t, err)
|
require.NoError(tb, err)
|
||||||
|
|
||||||
return &staticSalt{salt: s}
|
return &staticSalt{salt: s}
|
||||||
}
|
}
|
||||||
@@ -37,16 +37,16 @@ func (s *staticSalt) Salt(_ context.Context) (*salt.Salt, error) {
|
|||||||
|
|
||||||
type testingFormatWriter struct {
|
type testingFormatWriter struct {
|
||||||
salt *salt.Salt
|
salt *salt.Salt
|
||||||
lastRequest *AuditRequestEntry
|
lastRequest *RequestEntry
|
||||||
lastResponse *AuditResponseEntry
|
lastResponse *ResponseEntry
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fw *testingFormatWriter) WriteRequest(_ io.Writer, entry *AuditRequestEntry) error {
|
func (fw *testingFormatWriter) WriteRequest(_ io.Writer, entry *RequestEntry) error {
|
||||||
fw.lastRequest = entry
|
fw.lastRequest = entry
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (fw *testingFormatWriter) WriteResponse(_ io.Writer, entry *AuditResponseEntry) error {
|
func (fw *testingFormatWriter) WriteResponse(_ io.Writer, entry *ResponseEntry) error {
|
||||||
fw.lastResponse = entry
|
fw.lastResponse = entry
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -86,23 +86,26 @@ func (fw *testingFormatWriter) hashExpectedValueForComparison(input map[string]i
|
|||||||
return copiedAsMap
|
return copiedAsMap
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestNewAuditFormatter tests that creating a new AuditFormatter can be done safely.
|
// TestNewEntryFormatterWriter tests that creating a new EntryFormatterWriter can be done safely.
|
||||||
func TestNewAuditFormatter(t *testing.T) {
|
func TestNewEntryFormatterWriter(t *testing.T) {
|
||||||
tests := map[string]struct {
|
tests := map[string]struct {
|
||||||
Salter Salter
|
Salter Salter
|
||||||
UseStaticSalter bool
|
UseStaticSalter bool
|
||||||
IsErrorExpected bool
|
UseNilFormatter bool
|
||||||
ExpectedErrorMessag string
|
UseNilWriter bool
|
||||||
|
IsErrorExpected bool
|
||||||
|
ExpectedErrorMessage string
|
||||||
}{
|
}{
|
||||||
"nil": {
|
"nil": {
|
||||||
Salter: nil,
|
Salter: nil,
|
||||||
IsErrorExpected: true,
|
UseNilFormatter: true,
|
||||||
ExpectedErrorMessag: "cannot create a new audit formatter with nil salter",
|
UseNilWriter: true,
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "cannot create a new audit formatter with nil salter",
|
||||||
},
|
},
|
||||||
"static": {
|
"static": {
|
||||||
UseStaticSalter: true,
|
UseStaticSalter: true,
|
||||||
IsErrorExpected: false,
|
IsErrorExpected: false,
|
||||||
ExpectedErrorMessag: "",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -119,22 +122,38 @@ func TestNewAuditFormatter(t *testing.T) {
|
|||||||
s = tc.Salter
|
s = tc.Salter
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := NewAuditFormatter(s)
|
cfg, err := NewFormatterConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var f Formatter
|
||||||
|
if !tc.UseNilFormatter {
|
||||||
|
tempFormatter, err := NewEntryFormatter(cfg, s)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, tempFormatter)
|
||||||
|
f = tempFormatter
|
||||||
|
}
|
||||||
|
|
||||||
|
var w Writer
|
||||||
|
if !tc.UseNilWriter {
|
||||||
|
w = &JSONWriter{}
|
||||||
|
}
|
||||||
|
|
||||||
|
fw, err := NewEntryFormatterWriter(cfg, f, w)
|
||||||
switch {
|
switch {
|
||||||
case tc.IsErrorExpected:
|
case tc.IsErrorExpected:
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.Nil(t, f)
|
require.Nil(t, fw)
|
||||||
default:
|
default:
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.NotNil(t, f)
|
require.NotNil(t, fw)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAuditFormatter_FormatRequest exercises AuditFormatter.FormatRequest with
|
// TestEntryFormatter_FormatRequest exercises EntryFormatter.FormatRequest with
|
||||||
// varying inputs.
|
// varying inputs.
|
||||||
func TestAuditFormatter_FormatRequest(t *testing.T) {
|
func TestEntryFormatter_FormatRequest(t *testing.T) {
|
||||||
tests := map[string]struct {
|
tests := map[string]struct {
|
||||||
Input *logical.LogInput
|
Input *logical.LogInput
|
||||||
IsErrorExpected bool
|
IsErrorExpected bool
|
||||||
@@ -169,8 +188,10 @@ func TestAuditFormatter_FormatRequest(t *testing.T) {
|
|||||||
tc := tc
|
tc := tc
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
config := FormatterConfig{}
|
|
||||||
f, err := NewAuditFormatter(newStaticSalt(t))
|
cfg, err := NewFormatterConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
f, err := NewEntryFormatter(cfg, newStaticSalt(t))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var ctx context.Context
|
var ctx context.Context
|
||||||
@@ -181,7 +202,7 @@ func TestAuditFormatter_FormatRequest(t *testing.T) {
|
|||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err := f.FormatRequest(ctx, config, tc.Input)
|
entry, err := f.FormatRequest(ctx, tc.Input)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case tc.IsErrorExpected:
|
case tc.IsErrorExpected:
|
||||||
@@ -196,9 +217,9 @@ func TestAuditFormatter_FormatRequest(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestAuditFormatter_FormatResponse exercises AuditFormatter.FormatResponse with
|
// TestEntryFormatter_FormatResponse exercises EntryFormatter.FormatResponse with
|
||||||
// varying inputs.
|
// varying inputs.
|
||||||
func TestAuditFormatter_FormatResponse(t *testing.T) {
|
func TestEntryFormatter_FormatResponse(t *testing.T) {
|
||||||
tests := map[string]struct {
|
tests := map[string]struct {
|
||||||
Input *logical.LogInput
|
Input *logical.LogInput
|
||||||
IsErrorExpected bool
|
IsErrorExpected bool
|
||||||
@@ -233,8 +254,10 @@ func TestAuditFormatter_FormatResponse(t *testing.T) {
|
|||||||
tc := tc
|
tc := tc
|
||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
config := FormatterConfig{}
|
|
||||||
f, err := NewAuditFormatter(newStaticSalt(t))
|
cfg, err := NewFormatterConfig()
|
||||||
|
require.NoError(t, err)
|
||||||
|
f, err := NewEntryFormatter(cfg, newStaticSalt(t))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var ctx context.Context
|
var ctx context.Context
|
||||||
@@ -245,7 +268,7 @@ func TestAuditFormatter_FormatResponse(t *testing.T) {
|
|||||||
ctx = context.Background()
|
ctx = context.Background()
|
||||||
}
|
}
|
||||||
|
|
||||||
entry, err := f.FormatResponse(ctx, config, tc.Input)
|
entry, err := f.FormatResponse(ctx, tc.Input)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case tc.IsErrorExpected:
|
case tc.IsErrorExpected:
|
||||||
@@ -261,15 +284,6 @@ func TestAuditFormatter_FormatResponse(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestElideListResponses(t *testing.T) {
|
func TestElideListResponses(t *testing.T) {
|
||||||
tfw := testingFormatWriter{}
|
|
||||||
f, err := NewAuditFormatter(&tfw)
|
|
||||||
require.NoError(t, err)
|
|
||||||
formatter := AuditFormatterWriter{
|
|
||||||
Formatter: f,
|
|
||||||
Writer: &tfw,
|
|
||||||
}
|
|
||||||
ctx := namespace.RootContext(context.Background())
|
|
||||||
|
|
||||||
type test struct {
|
type test struct {
|
||||||
name string
|
name string
|
||||||
inputData map[string]interface{}
|
inputData map[string]interface{}
|
||||||
@@ -340,13 +354,17 @@ func TestElideListResponses(t *testing.T) {
|
|||||||
}
|
}
|
||||||
oneInterestingTestCase := tests[2]
|
oneInterestingTestCase := tests[2]
|
||||||
|
|
||||||
formatResponse := func(
|
tfw := testingFormatWriter{}
|
||||||
t *testing.T,
|
ctx := namespace.RootContext(context.Background())
|
||||||
config FormatterConfig,
|
|
||||||
operation logical.Operation,
|
formatResponse := func(t *testing.T, config FormatterConfig, operation logical.Operation, inputData map[string]interface{},
|
||||||
inputData map[string]interface{},
|
|
||||||
) {
|
) {
|
||||||
err := formatter.FormatAndWriteResponse(ctx, io.Discard, config, &logical.LogInput{
|
f, err := NewEntryFormatter(config, &tfw)
|
||||||
|
require.NoError(t, err)
|
||||||
|
formatter, err := NewEntryFormatterWriter(config, f, &tfw)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, formatter)
|
||||||
|
err = formatter.FormatAndWriteResponse(ctx, io.Discard, &logical.LogInput{
|
||||||
Request: &logical.Request{Operation: operation},
|
Request: &logical.Request{Operation: operation},
|
||||||
Response: &logical.Response{Data: inputData},
|
Response: &logical.Response{Data: inputData},
|
||||||
})
|
})
|
||||||
@@ -354,7 +372,8 @@ func TestElideListResponses(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
t.Run("Default case", func(t *testing.T) {
|
t.Run("Default case", func(t *testing.T) {
|
||||||
config := FormatterConfig{ElideListResponses: true}
|
config, err := NewFormatterConfig(WithElision(true))
|
||||||
|
require.NoError(t, err)
|
||||||
for _, tc := range tests {
|
for _, tc := range tests {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
formatResponse(t, config, logical.ListOperation, tc.inputData)
|
formatResponse(t, config, logical.ListOperation, tc.inputData)
|
||||||
@@ -364,21 +383,24 @@ func TestElideListResponses(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("When Operation is not list, eliding does not happen", func(t *testing.T) {
|
t.Run("When Operation is not list, eliding does not happen", func(t *testing.T) {
|
||||||
config := FormatterConfig{ElideListResponses: true}
|
config, err := NewFormatterConfig(WithElision(true))
|
||||||
|
require.NoError(t, err)
|
||||||
tc := oneInterestingTestCase
|
tc := oneInterestingTestCase
|
||||||
formatResponse(t, config, logical.ReadOperation, tc.inputData)
|
formatResponse(t, config, logical.ReadOperation, tc.inputData)
|
||||||
assert.Equal(t, tfw.hashExpectedValueForComparison(tc.inputData), tfw.lastResponse.Response.Data)
|
assert.Equal(t, tfw.hashExpectedValueForComparison(tc.inputData), tfw.lastResponse.Response.Data)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("When ElideListResponses is false, eliding does not happen", func(t *testing.T) {
|
t.Run("When ElideListResponses is false, eliding does not happen", func(t *testing.T) {
|
||||||
config := FormatterConfig{ElideListResponses: false}
|
config, err := NewFormatterConfig(WithElision(false), WithFormat(JSONFormat.String()))
|
||||||
|
require.NoError(t, err)
|
||||||
tc := oneInterestingTestCase
|
tc := oneInterestingTestCase
|
||||||
formatResponse(t, config, logical.ListOperation, tc.inputData)
|
formatResponse(t, config, logical.ListOperation, tc.inputData)
|
||||||
assert.Equal(t, tfw.hashExpectedValueForComparison(tc.inputData), tfw.lastResponse.Response.Data)
|
assert.Equal(t, tfw.hashExpectedValueForComparison(tc.inputData), tfw.lastResponse.Response.Data)
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("When Raw is true, eliding still happens", func(t *testing.T) {
|
t.Run("When Raw is true, eliding still happens", func(t *testing.T) {
|
||||||
config := FormatterConfig{ElideListResponses: true, Raw: true}
|
config, err := NewFormatterConfig(WithElision(true), WithRaw(true), WithFormat(JSONFormat.String()))
|
||||||
|
require.NoError(t, err)
|
||||||
tc := oneInterestingTestCase
|
tc := oneInterestingTestCase
|
||||||
formatResponse(t, config, logical.ListOperation, tc.inputData)
|
formatResponse(t, config, logical.ListOperation, tc.inputData)
|
||||||
assert.Equal(t, tc.expectedData, tfw.lastResponse.Response.Data)
|
assert.Equal(t, tc.expectedData, tfw.lastResponse.Response.Data)
|
||||||
182
audit/event.go
Normal file
182
audit/event.go
Normal file
@@ -0,0 +1,182 @@
|
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package audit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/internal/observability/event"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/sdk/helper/salt"
|
||||||
|
"github.com/hashicorp/vault/sdk/logical"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Audit subtypes.
|
||||||
|
const (
|
||||||
|
RequestType subtype = "AuditRequest"
|
||||||
|
ResponseType subtype = "AuditResponse"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Audit formats.
|
||||||
|
const (
|
||||||
|
JSONFormat format = "json"
|
||||||
|
JSONxFormat format = "jsonx"
|
||||||
|
)
|
||||||
|
|
||||||
|
// version defines the version of audit events.
|
||||||
|
const version = "v0.1"
|
||||||
|
|
||||||
|
// subtype defines the type of audit event.
|
||||||
|
type subtype string
|
||||||
|
|
||||||
|
// format defines types of format audit events support.
|
||||||
|
type format string
|
||||||
|
|
||||||
|
// Backend interface must be implemented for an audit
|
||||||
|
// mechanism to be made available. Audit backends can be enabled to
|
||||||
|
// sink information to different backends such as logs, file, databases,
|
||||||
|
// or other external services.
|
||||||
|
type Backend interface {
|
||||||
|
// LogRequest is used to synchronously log a request. This is done after the
|
||||||
|
// request is authorized but before the request is executed. The arguments
|
||||||
|
// MUST not be modified in anyway. They should be deep copied if this is
|
||||||
|
// a possibility.
|
||||||
|
LogRequest(context.Context, *logical.LogInput) error
|
||||||
|
|
||||||
|
// LogResponse is used to synchronously log a response. This is done after
|
||||||
|
// the request is processed but before the response is sent. The arguments
|
||||||
|
// MUST not be modified in anyway. They should be deep copied if this is
|
||||||
|
// a possibility.
|
||||||
|
LogResponse(context.Context, *logical.LogInput) error
|
||||||
|
|
||||||
|
// LogTestMessage is used to check an audit backend before adding it
|
||||||
|
// permanently. It should attempt to synchronously log the given test
|
||||||
|
// message, WITHOUT using the normal Salt (which would require a storage
|
||||||
|
// operation on creation, which is currently disallowed.)
|
||||||
|
LogTestMessage(context.Context, *logical.LogInput, map[string]string) error
|
||||||
|
|
||||||
|
// GetHash is used to return the given data with the backend's hash,
|
||||||
|
// so that a caller can determine if a value in the audit log matches
|
||||||
|
// an expected plaintext value
|
||||||
|
GetHash(context.Context, string) (string, error)
|
||||||
|
|
||||||
|
// Reload is called on SIGHUP for supporting backends.
|
||||||
|
Reload(context.Context) error
|
||||||
|
|
||||||
|
// Invalidate is called for path invalidation
|
||||||
|
Invalidate(context.Context)
|
||||||
|
}
|
||||||
|
|
||||||
|
// BackendConfig contains configuration parameters used in the factory func to
|
||||||
|
// instantiate audit backends
|
||||||
|
type BackendConfig struct {
|
||||||
|
// The view to store the salt
|
||||||
|
SaltView logical.Storage
|
||||||
|
|
||||||
|
// The salt config that should be used for any secret obfuscation
|
||||||
|
SaltConfig *salt.Config
|
||||||
|
|
||||||
|
// Config is the opaque user configuration provided when mounting
|
||||||
|
Config map[string]string
|
||||||
|
}
|
||||||
|
|
||||||
|
// Factory is the factory function to create an audit backend.
|
||||||
|
type Factory func(context.Context, *BackendConfig, bool) (Backend, error)
|
||||||
|
|
||||||
|
// newEvent should be used to create an audit event.
|
||||||
|
// subtype and format are needed for audit.
|
||||||
|
// It will generate an ID if no ID is supplied.
|
||||||
|
// Supported options: WithID, WithNow.
|
||||||
|
func newEvent(s subtype, f format, opt ...Option) (*auditEvent, error) {
|
||||||
|
const op = "audit.newEvent"
|
||||||
|
|
||||||
|
// Get the default options
|
||||||
|
opts, err := getOpts(opt...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: error applying options: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if opts.withID == "" {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
opts.withID, err = event.NewID(string(event.AuditType))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: error creating ID for event: %w", op, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
audit := &auditEvent{
|
||||||
|
ID: opts.withID,
|
||||||
|
Timestamp: opts.withNow,
|
||||||
|
Version: version,
|
||||||
|
Subtype: s,
|
||||||
|
RequiredFormat: f,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := audit.validate(); err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
return audit, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate attempts to ensure the audit event in its present state is valid.
|
||||||
|
func (a *auditEvent) validate() error {
|
||||||
|
const op = "audit.(auditEvent).validate"
|
||||||
|
|
||||||
|
if a == nil {
|
||||||
|
return fmt.Errorf("%s: event is nil: %w", op, event.ErrInvalidParameter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.ID == "" {
|
||||||
|
return fmt.Errorf("%s: missing ID: %w", op, event.ErrInvalidParameter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Version != version {
|
||||||
|
return fmt.Errorf("%s: event version unsupported: %w", op, event.ErrInvalidParameter)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.Timestamp.IsZero() {
|
||||||
|
return fmt.Errorf("%s: event timestamp cannot be the zero time instant: %w", op, event.ErrInvalidParameter)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := a.Subtype.validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.RequiredFormat.validate()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate ensures that subtype is one of the set of allowed event subtypes.
|
||||||
|
func (t subtype) validate() error {
|
||||||
|
const op = "audit.(subtype).validate"
|
||||||
|
switch t {
|
||||||
|
case RequestType, ResponseType:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%s: '%s' is not a valid event subtype: %w", op, t, event.ErrInvalidParameter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// validate ensures that format is one of the set of allowed event formats.
|
||||||
|
func (f format) validate() error {
|
||||||
|
const op = "audit.(format).validate"
|
||||||
|
switch f {
|
||||||
|
case JSONFormat, JSONxFormat:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("%s: '%s' is not a valid format: %w", op, f, event.ErrInvalidParameter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String returns the string version of a format.
|
||||||
|
func (f format) String() string {
|
||||||
|
return string(f)
|
||||||
|
}
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
// Copyright (c) HashiCorp, Inc.
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
package event
|
package audit
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@@ -10,56 +10,68 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestAuditEvent_New exercises the newAudit func to create audit events.
|
// TestAuditEvent_new exercises the newEvent func to create audit events.
|
||||||
func TestAuditEvent_New(t *testing.T) {
|
func TestAuditEvent_new(t *testing.T) {
|
||||||
tests := map[string]struct {
|
tests := map[string]struct {
|
||||||
Options []Option
|
Options []Option
|
||||||
|
Subtype subtype
|
||||||
|
Format format
|
||||||
IsErrorExpected bool
|
IsErrorExpected bool
|
||||||
ExpectedErrorMessage string
|
ExpectedErrorMessage string
|
||||||
ExpectedID string
|
ExpectedID string
|
||||||
ExpectedFormat auditFormat
|
ExpectedFormat format
|
||||||
ExpectedSubtype auditSubtype
|
ExpectedSubtype subtype
|
||||||
ExpectedTimestamp time.Time
|
ExpectedTimestamp time.Time
|
||||||
IsNowExpected bool
|
IsNowExpected bool
|
||||||
}{
|
}{
|
||||||
"nil": {
|
"nil": {
|
||||||
Options: nil,
|
Options: nil,
|
||||||
|
Subtype: subtype(""),
|
||||||
|
Format: format(""),
|
||||||
IsErrorExpected: true,
|
IsErrorExpected: true,
|
||||||
ExpectedErrorMessage: "event.newAudit: event.(audit).validate: event.(auditSubtype).validate: '' is not a valid event subtype: invalid parameter",
|
ExpectedErrorMessage: "audit.newEvent: audit.(auditEvent).validate: audit.(subtype).validate: '' is not a valid event subtype: invalid parameter",
|
||||||
},
|
},
|
||||||
"empty-option": {
|
"empty-Option": {
|
||||||
Options: []Option{},
|
Options: []Option{},
|
||||||
|
Subtype: subtype(""),
|
||||||
|
Format: format(""),
|
||||||
IsErrorExpected: true,
|
IsErrorExpected: true,
|
||||||
ExpectedErrorMessage: "event.newAudit: event.(audit).validate: event.(auditSubtype).validate: '' is not a valid event subtype: invalid parameter",
|
ExpectedErrorMessage: "audit.newEvent: audit.(auditEvent).validate: audit.(subtype).validate: '' is not a valid event subtype: invalid parameter",
|
||||||
},
|
},
|
||||||
"bad-id": {
|
"bad-id": {
|
||||||
Options: []Option{WithID("")},
|
Options: []Option{WithID("")},
|
||||||
|
Subtype: ResponseType,
|
||||||
|
Format: JSONFormat,
|
||||||
IsErrorExpected: true,
|
IsErrorExpected: true,
|
||||||
ExpectedErrorMessage: "event.newAudit: error applying options: id cannot be empty",
|
ExpectedErrorMessage: "audit.newEvent: error applying options: id cannot be empty",
|
||||||
},
|
},
|
||||||
"good": {
|
"good": {
|
||||||
Options: []Option{
|
Options: []Option{
|
||||||
WithID("audit_123"),
|
WithID("audit_123"),
|
||||||
WithFormat(string(AuditFormatJSON)),
|
WithFormat(string(JSONFormat)),
|
||||||
WithSubtype(string(AuditResponse)),
|
WithSubtype(string(ResponseType)),
|
||||||
WithNow(time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local)),
|
WithNow(time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local)),
|
||||||
},
|
},
|
||||||
|
Subtype: RequestType,
|
||||||
|
Format: JSONxFormat,
|
||||||
IsErrorExpected: false,
|
IsErrorExpected: false,
|
||||||
ExpectedID: "audit_123",
|
ExpectedID: "audit_123",
|
||||||
ExpectedTimestamp: time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local),
|
ExpectedTimestamp: time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local),
|
||||||
ExpectedSubtype: AuditResponse,
|
ExpectedSubtype: RequestType,
|
||||||
ExpectedFormat: AuditFormatJSON,
|
ExpectedFormat: JSONxFormat,
|
||||||
},
|
},
|
||||||
"good-no-time": {
|
"good-no-time": {
|
||||||
Options: []Option{
|
Options: []Option{
|
||||||
WithID("audit_123"),
|
WithID("audit_123"),
|
||||||
WithFormat(string(AuditFormatJSON)),
|
WithFormat(string(JSONFormat)),
|
||||||
WithSubtype(string(AuditResponse)),
|
WithSubtype(string(ResponseType)),
|
||||||
},
|
},
|
||||||
|
Subtype: RequestType,
|
||||||
|
Format: JSONxFormat,
|
||||||
IsErrorExpected: false,
|
IsErrorExpected: false,
|
||||||
ExpectedID: "audit_123",
|
ExpectedID: "audit_123",
|
||||||
ExpectedSubtype: AuditResponse,
|
ExpectedSubtype: RequestType,
|
||||||
ExpectedFormat: AuditFormatJSON,
|
ExpectedFormat: JSONxFormat,
|
||||||
IsNowExpected: true,
|
IsNowExpected: true,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -70,7 +82,7 @@ func TestAuditEvent_New(t *testing.T) {
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
audit, err := newAudit(tc.Options...)
|
audit, err := newEvent(tc.Subtype, tc.Format, tc.Options...)
|
||||||
switch {
|
switch {
|
||||||
case tc.IsErrorExpected:
|
case tc.IsErrorExpected:
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -97,88 +109,88 @@ func TestAuditEvent_New(t *testing.T) {
|
|||||||
// TestAuditEvent_Validate exercises the validation for an audit event.
|
// TestAuditEvent_Validate exercises the validation for an audit event.
|
||||||
func TestAuditEvent_Validate(t *testing.T) {
|
func TestAuditEvent_Validate(t *testing.T) {
|
||||||
tests := map[string]struct {
|
tests := map[string]struct {
|
||||||
Value *audit
|
Value *auditEvent
|
||||||
IsErrorExpected bool
|
IsErrorExpected bool
|
||||||
ExpectedErrorMessage string
|
ExpectedErrorMessage string
|
||||||
}{
|
}{
|
||||||
"nil": {
|
"nil": {
|
||||||
Value: nil,
|
Value: nil,
|
||||||
IsErrorExpected: true,
|
IsErrorExpected: true,
|
||||||
ExpectedErrorMessage: "event.(audit).validate: audit is nil: invalid parameter",
|
ExpectedErrorMessage: "audit.(auditEvent).validate: event is nil: invalid parameter",
|
||||||
},
|
},
|
||||||
"default": {
|
"default": {
|
||||||
Value: &audit{},
|
Value: &auditEvent{},
|
||||||
IsErrorExpected: true,
|
IsErrorExpected: true,
|
||||||
ExpectedErrorMessage: "event.(audit).validate: missing ID: invalid parameter",
|
ExpectedErrorMessage: "audit.(auditEvent).validate: missing ID: invalid parameter",
|
||||||
},
|
},
|
||||||
"id-empty": {
|
"id-empty": {
|
||||||
Value: &audit{
|
Value: &auditEvent{
|
||||||
ID: "",
|
ID: "",
|
||||||
Version: auditVersion,
|
Version: version,
|
||||||
Subtype: AuditRequest,
|
Subtype: RequestType,
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
Data: nil,
|
Data: nil,
|
||||||
RequiredFormat: AuditFormatJSON,
|
RequiredFormat: JSONFormat,
|
||||||
},
|
},
|
||||||
IsErrorExpected: true,
|
IsErrorExpected: true,
|
||||||
ExpectedErrorMessage: "event.(audit).validate: missing ID: invalid parameter",
|
ExpectedErrorMessage: "audit.(auditEvent).validate: missing ID: invalid parameter",
|
||||||
},
|
},
|
||||||
"version-fiddled": {
|
"version-fiddled": {
|
||||||
Value: &audit{
|
Value: &auditEvent{
|
||||||
ID: "audit_123",
|
ID: "audit_123",
|
||||||
Version: "magic-v2",
|
Version: "magic-v2",
|
||||||
Subtype: AuditRequest,
|
Subtype: RequestType,
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
Data: nil,
|
Data: nil,
|
||||||
RequiredFormat: AuditFormatJSON,
|
RequiredFormat: JSONFormat,
|
||||||
},
|
},
|
||||||
IsErrorExpected: true,
|
IsErrorExpected: true,
|
||||||
ExpectedErrorMessage: "event.(audit).validate: audit version unsupported: invalid parameter",
|
ExpectedErrorMessage: "audit.(auditEvent).validate: event version unsupported: invalid parameter",
|
||||||
},
|
},
|
||||||
"subtype-fiddled": {
|
"subtype-fiddled": {
|
||||||
Value: &audit{
|
Value: &auditEvent{
|
||||||
ID: "audit_123",
|
ID: "audit_123",
|
||||||
Version: auditVersion,
|
Version: version,
|
||||||
Subtype: auditSubtype("moon"),
|
Subtype: subtype("moon"),
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
Data: nil,
|
Data: nil,
|
||||||
RequiredFormat: AuditFormatJSON,
|
RequiredFormat: JSONFormat,
|
||||||
},
|
},
|
||||||
IsErrorExpected: true,
|
IsErrorExpected: true,
|
||||||
ExpectedErrorMessage: "event.(audit).validate: event.(auditSubtype).validate: 'moon' is not a valid event subtype: invalid parameter",
|
ExpectedErrorMessage: "audit.(auditEvent).validate: audit.(subtype).validate: 'moon' is not a valid event subtype: invalid parameter",
|
||||||
},
|
},
|
||||||
"format-fiddled": {
|
"format-fiddled": {
|
||||||
Value: &audit{
|
Value: &auditEvent{
|
||||||
ID: "audit_123",
|
ID: "audit_123",
|
||||||
Version: auditVersion,
|
Version: version,
|
||||||
Subtype: AuditResponse,
|
Subtype: ResponseType,
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
Data: nil,
|
Data: nil,
|
||||||
RequiredFormat: auditFormat("blah"),
|
RequiredFormat: format("blah"),
|
||||||
},
|
},
|
||||||
IsErrorExpected: true,
|
IsErrorExpected: true,
|
||||||
ExpectedErrorMessage: "event.(audit).validate: event.(auditFormat).validate: 'blah' is not a valid format: invalid parameter",
|
ExpectedErrorMessage: "audit.(auditEvent).validate: audit.(format).validate: 'blah' is not a valid format: invalid parameter",
|
||||||
},
|
},
|
||||||
"default-time": {
|
"default-time": {
|
||||||
Value: &audit{
|
Value: &auditEvent{
|
||||||
ID: "audit_123",
|
ID: "audit_123",
|
||||||
Version: auditVersion,
|
Version: version,
|
||||||
Subtype: AuditResponse,
|
Subtype: ResponseType,
|
||||||
Timestamp: time.Time{},
|
Timestamp: time.Time{},
|
||||||
Data: nil,
|
Data: nil,
|
||||||
RequiredFormat: AuditFormatJSON,
|
RequiredFormat: JSONFormat,
|
||||||
},
|
},
|
||||||
IsErrorExpected: true,
|
IsErrorExpected: true,
|
||||||
ExpectedErrorMessage: "event.(audit).validate: audit timestamp cannot be the zero time instant: invalid parameter",
|
ExpectedErrorMessage: "audit.(auditEvent).validate: event timestamp cannot be the zero time instant: invalid parameter",
|
||||||
},
|
},
|
||||||
"valid": {
|
"valid": {
|
||||||
Value: &audit{
|
Value: &auditEvent{
|
||||||
ID: "audit_123",
|
ID: "audit_123",
|
||||||
Version: auditVersion,
|
Version: version,
|
||||||
Subtype: AuditResponse,
|
Subtype: ResponseType,
|
||||||
Timestamp: time.Now(),
|
Timestamp: time.Now(),
|
||||||
Data: nil,
|
Data: nil,
|
||||||
RequiredFormat: AuditFormatJSON,
|
RequiredFormat: JSONFormat,
|
||||||
},
|
},
|
||||||
IsErrorExpected: false,
|
IsErrorExpected: false,
|
||||||
},
|
},
|
||||||
@@ -212,12 +224,12 @@ func TestAuditEvent_Validate_Subtype(t *testing.T) {
|
|||||||
"empty": {
|
"empty": {
|
||||||
Value: "",
|
Value: "",
|
||||||
IsErrorExpected: true,
|
IsErrorExpected: true,
|
||||||
ExpectedErrorMessage: "event.(auditSubtype).validate: '' is not a valid event subtype: invalid parameter",
|
ExpectedErrorMessage: "audit.(subtype).validate: '' is not a valid event subtype: invalid parameter",
|
||||||
},
|
},
|
||||||
"unsupported": {
|
"unsupported": {
|
||||||
Value: "foo",
|
Value: "foo",
|
||||||
IsErrorExpected: true,
|
IsErrorExpected: true,
|
||||||
ExpectedErrorMessage: "event.(auditSubtype).validate: 'foo' is not a valid event subtype: invalid parameter",
|
ExpectedErrorMessage: "audit.(subtype).validate: 'foo' is not a valid event subtype: invalid parameter",
|
||||||
},
|
},
|
||||||
"request": {
|
"request": {
|
||||||
Value: "AuditRequest",
|
Value: "AuditRequest",
|
||||||
@@ -235,7 +247,7 @@ func TestAuditEvent_Validate_Subtype(t *testing.T) {
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
err := auditSubtype(tc.Value).validate()
|
err := subtype(tc.Value).validate()
|
||||||
switch {
|
switch {
|
||||||
case tc.IsErrorExpected:
|
case tc.IsErrorExpected:
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
@@ -257,12 +269,12 @@ func TestAuditEvent_Validate_Format(t *testing.T) {
|
|||||||
"empty": {
|
"empty": {
|
||||||
Value: "",
|
Value: "",
|
||||||
IsErrorExpected: true,
|
IsErrorExpected: true,
|
||||||
ExpectedErrorMessage: "event.(auditFormat).validate: '' is not a valid format: invalid parameter",
|
ExpectedErrorMessage: "audit.(format).validate: '' is not a valid format: invalid parameter",
|
||||||
},
|
},
|
||||||
"unsupported": {
|
"unsupported": {
|
||||||
Value: "foo",
|
Value: "foo",
|
||||||
IsErrorExpected: true,
|
IsErrorExpected: true,
|
||||||
ExpectedErrorMessage: "event.(auditFormat).validate: 'foo' is not a valid format: invalid parameter",
|
ExpectedErrorMessage: "audit.(format).validate: 'foo' is not a valid format: invalid parameter",
|
||||||
},
|
},
|
||||||
"json": {
|
"json": {
|
||||||
Value: "json",
|
Value: "json",
|
||||||
@@ -280,7 +292,7 @@ func TestAuditEvent_Validate_Format(t *testing.T) {
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
err := auditFormat(tc.Value).validate()
|
err := format(tc.Value).validate()
|
||||||
switch {
|
switch {
|
||||||
case tc.IsErrorExpected:
|
case tc.IsErrorExpected:
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
716
audit/format.go
716
audit/format.go
@@ -1,716 +0,0 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package audit
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-jose/go-jose/v3/jwt"
|
|
||||||
|
|
||||||
"github.com/hashicorp/vault/helper/namespace"
|
|
||||||
"github.com/hashicorp/vault/sdk/helper/salt"
|
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Salter is an interface that provides a way to obtain a Salt for hashing.
|
|
||||||
type Salter interface {
|
|
||||||
// Salt returns a non-nil salt or an error.
|
|
||||||
Salt(context.Context) (*salt.Salt, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Formatter is an interface that is responsible for formatting a request/response into some format.
|
|
||||||
// It is recommended that you pass data through Hash prior to formatting it.
|
|
||||||
type Formatter interface {
|
|
||||||
// FormatRequest formats the logical.LogInput into an AuditRequestEntry.
|
|
||||||
FormatRequest(context.Context, FormatterConfig, *logical.LogInput) (*AuditRequestEntry, error)
|
|
||||||
// FormatResponse formats the logical.LogInput into an AuditResponseEntry.
|
|
||||||
FormatResponse(context.Context, FormatterConfig, *logical.LogInput) (*AuditResponseEntry, error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Writer is an interface that provides a way to write request and response audit entries.
|
|
||||||
// Formatters write their output to an io.Writer.
|
|
||||||
type Writer interface {
|
|
||||||
// WriteRequest writes the request entry to the writer or returns an error.
|
|
||||||
WriteRequest(io.Writer, *AuditRequestEntry) error
|
|
||||||
// WriteResponse writes the response entry to the writer or returns an error.
|
|
||||||
WriteResponse(io.Writer, *AuditResponseEntry) error
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
_ Formatter = (*AuditFormatter)(nil)
|
|
||||||
_ Formatter = (*AuditFormatterWriter)(nil)
|
|
||||||
_ Writer = (*AuditFormatterWriter)(nil)
|
|
||||||
)
|
|
||||||
|
|
||||||
// AuditFormatter should be used to format audit requests and responses.
|
|
||||||
type AuditFormatter struct {
|
|
||||||
salter Salter
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuditFormatterWriter should be used to format and write out audit requests and responses.
|
|
||||||
type AuditFormatterWriter struct {
|
|
||||||
Formatter
|
|
||||||
Writer
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// nonPersistentSalt is used for obtaining a salt that is
|
|
||||||
type nonPersistentSalt struct{}
|
|
||||||
|
|
||||||
// Salt returns a new salt with default configuration and no storage usage, and no error.
|
|
||||||
func (s *nonPersistentSalt) Salt(_ context.Context) (*salt.Salt, error) {
|
|
||||||
return salt.NewNonpersistentSalt(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAuditFormatter should be used to create an AuditFormatter.
|
|
||||||
func NewAuditFormatter(salter Salter) (*AuditFormatter, error) {
|
|
||||||
if salter == nil {
|
|
||||||
return nil, errors.New("cannot create a new audit formatter with nil salter")
|
|
||||||
}
|
|
||||||
|
|
||||||
return &AuditFormatter{salter: salter}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAuditFormatterWriter should be used to create a new AuditFormatterWriter.
|
|
||||||
func NewAuditFormatterWriter(formatter Formatter, writer Writer) (*AuditFormatterWriter, error) {
|
|
||||||
switch {
|
|
||||||
case formatter == nil:
|
|
||||||
return nil, errors.New("cannot create a new audit formatter writer with nil formatter")
|
|
||||||
case writer == nil:
|
|
||||||
return nil, errors.New("cannot create a new audit formatter writer with nil formatter")
|
|
||||||
}
|
|
||||||
|
|
||||||
fw := &AuditFormatterWriter{
|
|
||||||
Formatter: formatter,
|
|
||||||
Writer: writer,
|
|
||||||
}
|
|
||||||
|
|
||||||
return fw, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatRequest attempts to format the specified logical.LogInput into an AuditRequestEntry.
|
|
||||||
func (f *AuditFormatter) FormatRequest(ctx context.Context, config FormatterConfig, in *logical.LogInput) (*AuditRequestEntry, error) {
|
|
||||||
switch {
|
|
||||||
case in == nil || in.Request == nil:
|
|
||||||
return nil, errors.New("request to request-audit a nil request")
|
|
||||||
case f.salter == nil:
|
|
||||||
return nil, errors.New("salt func not configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := f.salter.Salt(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching salt: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set these to the input values at first
|
|
||||||
auth := in.Auth
|
|
||||||
req := in.Request
|
|
||||||
var connState *tls.ConnectionState
|
|
||||||
if auth == nil {
|
|
||||||
auth = new(logical.Auth)
|
|
||||||
}
|
|
||||||
|
|
||||||
if in.Request.Connection != nil && in.Request.Connection.ConnState != nil {
|
|
||||||
connState = in.Request.Connection.ConnState
|
|
||||||
}
|
|
||||||
|
|
||||||
if !config.Raw {
|
|
||||||
auth, err = HashAuth(s, auth, config.HMACAccessor)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err = HashRequest(s, req, config.HMACAccessor, in.NonHMACReqDataKeys)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var errString string
|
|
||||||
if in.OuterErr != nil {
|
|
||||||
errString = in.OuterErr.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
ns, err := namespace.FromContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
reqType := in.Type
|
|
||||||
if reqType == "" {
|
|
||||||
reqType = "request"
|
|
||||||
}
|
|
||||||
reqEntry := &AuditRequestEntry{
|
|
||||||
Type: reqType,
|
|
||||||
Error: errString,
|
|
||||||
ForwardedFrom: req.ForwardedFrom,
|
|
||||||
Auth: &AuditAuth{
|
|
||||||
ClientToken: auth.ClientToken,
|
|
||||||
Accessor: auth.Accessor,
|
|
||||||
DisplayName: auth.DisplayName,
|
|
||||||
Policies: auth.Policies,
|
|
||||||
TokenPolicies: auth.TokenPolicies,
|
|
||||||
IdentityPolicies: auth.IdentityPolicies,
|
|
||||||
ExternalNamespacePolicies: auth.ExternalNamespacePolicies,
|
|
||||||
NoDefaultPolicy: auth.NoDefaultPolicy,
|
|
||||||
Metadata: auth.Metadata,
|
|
||||||
EntityID: auth.EntityID,
|
|
||||||
RemainingUses: req.ClientTokenRemainingUses,
|
|
||||||
TokenType: auth.TokenType.String(),
|
|
||||||
TokenTTL: int64(auth.TTL.Seconds()),
|
|
||||||
},
|
|
||||||
|
|
||||||
Request: &AuditRequest{
|
|
||||||
ID: req.ID,
|
|
||||||
ClientID: req.ClientID,
|
|
||||||
ClientToken: req.ClientToken,
|
|
||||||
ClientTokenAccessor: req.ClientTokenAccessor,
|
|
||||||
Operation: req.Operation,
|
|
||||||
MountPoint: req.MountPoint,
|
|
||||||
MountType: req.MountType,
|
|
||||||
MountAccessor: req.MountAccessor,
|
|
||||||
MountRunningVersion: req.MountRunningVersion(),
|
|
||||||
MountRunningSha256: req.MountRunningSha256(),
|
|
||||||
MountIsExternalPlugin: req.MountIsExternalPlugin(),
|
|
||||||
MountClass: req.MountClass(),
|
|
||||||
Namespace: &AuditNamespace{
|
|
||||||
ID: ns.ID,
|
|
||||||
Path: ns.Path,
|
|
||||||
},
|
|
||||||
Path: req.Path,
|
|
||||||
Data: req.Data,
|
|
||||||
PolicyOverride: req.PolicyOverride,
|
|
||||||
RemoteAddr: getRemoteAddr(req),
|
|
||||||
RemotePort: getRemotePort(req),
|
|
||||||
ReplicationCluster: req.ReplicationCluster,
|
|
||||||
Headers: req.Headers,
|
|
||||||
ClientCertificateSerialNumber: getClientCertificateSerialNumber(connState),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if !auth.IssueTime.IsZero() {
|
|
||||||
reqEntry.Auth.TokenIssueTime = auth.IssueTime.Format(time.RFC3339)
|
|
||||||
}
|
|
||||||
|
|
||||||
if auth.PolicyResults != nil {
|
|
||||||
reqEntry.Auth.PolicyResults = &AuditPolicyResults{
|
|
||||||
Allowed: auth.PolicyResults.Allowed,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range auth.PolicyResults.GrantingPolicies {
|
|
||||||
reqEntry.Auth.PolicyResults.GrantingPolicies = append(reqEntry.Auth.PolicyResults.GrantingPolicies, PolicyInfo{
|
|
||||||
Name: p.Name,
|
|
||||||
NamespaceId: p.NamespaceId,
|
|
||||||
NamespacePath: p.NamespacePath,
|
|
||||||
Type: p.Type,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if req.WrapInfo != nil {
|
|
||||||
reqEntry.Request.WrapTTL = int(req.WrapInfo.TTL / time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !config.OmitTime {
|
|
||||||
reqEntry.Time = time.Now().UTC().Format(time.RFC3339Nano)
|
|
||||||
}
|
|
||||||
|
|
||||||
return reqEntry, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatResponse attempts to format the specified logical.LogInput into an AuditResponseEntry.
|
|
||||||
func (f *AuditFormatter) FormatResponse(ctx context.Context, config FormatterConfig, in *logical.LogInput) (*AuditResponseEntry, error) {
|
|
||||||
switch {
|
|
||||||
case in == nil || in.Request == nil:
|
|
||||||
return nil, errors.New("request to response-audit a nil request")
|
|
||||||
case f.salter == nil:
|
|
||||||
return nil, errors.New("salt func not configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := f.salter.Salt(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("error fetching salt: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Set these to the input values at first
|
|
||||||
auth, req, resp := in.Auth, in.Request, in.Response
|
|
||||||
if auth == nil {
|
|
||||||
auth = new(logical.Auth)
|
|
||||||
}
|
|
||||||
if resp == nil {
|
|
||||||
resp = new(logical.Response)
|
|
||||||
}
|
|
||||||
var connState *tls.ConnectionState
|
|
||||||
|
|
||||||
if in.Request.Connection != nil && in.Request.Connection.ConnState != nil {
|
|
||||||
connState = in.Request.Connection.ConnState
|
|
||||||
}
|
|
||||||
|
|
||||||
elideListResponseData := config.ElideListResponses && req.Operation == logical.ListOperation
|
|
||||||
|
|
||||||
var respData map[string]interface{}
|
|
||||||
if config.Raw {
|
|
||||||
// In the non-raw case, elision of list response data occurs inside HashResponse, to avoid redundant deep
|
|
||||||
// copies and hashing of data only to elide it later. In the raw case, we need to do it here.
|
|
||||||
if elideListResponseData && resp.Data != nil {
|
|
||||||
// Copy the data map before making changes, but we only need to go one level deep in this case
|
|
||||||
respData = make(map[string]interface{}, len(resp.Data))
|
|
||||||
for k, v := range resp.Data {
|
|
||||||
respData[k] = v
|
|
||||||
}
|
|
||||||
|
|
||||||
doElideListResponseData(respData)
|
|
||||||
} else {
|
|
||||||
respData = resp.Data
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
auth, err = HashAuth(s, auth, config.HMACAccessor)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
req, err = HashRequest(s, req, config.HMACAccessor, in.NonHMACReqDataKeys)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err = HashResponse(s, resp, config.HMACAccessor, in.NonHMACRespDataKeys, elideListResponseData)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
respData = resp.Data
|
|
||||||
}
|
|
||||||
|
|
||||||
var errString string
|
|
||||||
if in.OuterErr != nil {
|
|
||||||
errString = in.OuterErr.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
ns, err := namespace.FromContext(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
var respAuth *AuditAuth
|
|
||||||
if resp.Auth != nil {
|
|
||||||
respAuth = &AuditAuth{
|
|
||||||
ClientToken: resp.Auth.ClientToken,
|
|
||||||
Accessor: resp.Auth.Accessor,
|
|
||||||
DisplayName: resp.Auth.DisplayName,
|
|
||||||
Policies: resp.Auth.Policies,
|
|
||||||
TokenPolicies: resp.Auth.TokenPolicies,
|
|
||||||
IdentityPolicies: resp.Auth.IdentityPolicies,
|
|
||||||
ExternalNamespacePolicies: resp.Auth.ExternalNamespacePolicies,
|
|
||||||
NoDefaultPolicy: resp.Auth.NoDefaultPolicy,
|
|
||||||
Metadata: resp.Auth.Metadata,
|
|
||||||
NumUses: resp.Auth.NumUses,
|
|
||||||
EntityID: resp.Auth.EntityID,
|
|
||||||
TokenType: resp.Auth.TokenType.String(),
|
|
||||||
TokenTTL: int64(resp.Auth.TTL.Seconds()),
|
|
||||||
}
|
|
||||||
if !resp.Auth.IssueTime.IsZero() {
|
|
||||||
respAuth.TokenIssueTime = resp.Auth.IssueTime.Format(time.RFC3339)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var respSecret *AuditSecret
|
|
||||||
if resp.Secret != nil {
|
|
||||||
respSecret = &AuditSecret{
|
|
||||||
LeaseID: resp.Secret.LeaseID,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var respWrapInfo *AuditResponseWrapInfo
|
|
||||||
if resp.WrapInfo != nil {
|
|
||||||
token := resp.WrapInfo.Token
|
|
||||||
if jwtToken := parseVaultTokenFromJWT(token); jwtToken != nil {
|
|
||||||
token = *jwtToken
|
|
||||||
}
|
|
||||||
respWrapInfo = &AuditResponseWrapInfo{
|
|
||||||
TTL: int(resp.WrapInfo.TTL / time.Second),
|
|
||||||
Token: token,
|
|
||||||
Accessor: resp.WrapInfo.Accessor,
|
|
||||||
CreationTime: resp.WrapInfo.CreationTime.UTC().Format(time.RFC3339Nano),
|
|
||||||
CreationPath: resp.WrapInfo.CreationPath,
|
|
||||||
WrappedAccessor: resp.WrapInfo.WrappedAccessor,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
respType := in.Type
|
|
||||||
if respType == "" {
|
|
||||||
respType = "response"
|
|
||||||
}
|
|
||||||
respEntry := &AuditResponseEntry{
|
|
||||||
Type: respType,
|
|
||||||
Error: errString,
|
|
||||||
Forwarded: req.ForwardedFrom != "",
|
|
||||||
Auth: &AuditAuth{
|
|
||||||
ClientToken: auth.ClientToken,
|
|
||||||
Accessor: auth.Accessor,
|
|
||||||
DisplayName: auth.DisplayName,
|
|
||||||
Policies: auth.Policies,
|
|
||||||
TokenPolicies: auth.TokenPolicies,
|
|
||||||
IdentityPolicies: auth.IdentityPolicies,
|
|
||||||
ExternalNamespacePolicies: auth.ExternalNamespacePolicies,
|
|
||||||
NoDefaultPolicy: auth.NoDefaultPolicy,
|
|
||||||
Metadata: auth.Metadata,
|
|
||||||
RemainingUses: req.ClientTokenRemainingUses,
|
|
||||||
EntityID: auth.EntityID,
|
|
||||||
EntityCreated: auth.EntityCreated,
|
|
||||||
TokenType: auth.TokenType.String(),
|
|
||||||
TokenTTL: int64(auth.TTL.Seconds()),
|
|
||||||
},
|
|
||||||
|
|
||||||
Request: &AuditRequest{
|
|
||||||
ID: req.ID,
|
|
||||||
ClientToken: req.ClientToken,
|
|
||||||
ClientTokenAccessor: req.ClientTokenAccessor,
|
|
||||||
ClientID: req.ClientID,
|
|
||||||
Operation: req.Operation,
|
|
||||||
MountPoint: req.MountPoint,
|
|
||||||
MountType: req.MountType,
|
|
||||||
MountAccessor: req.MountAccessor,
|
|
||||||
MountRunningVersion: req.MountRunningVersion(),
|
|
||||||
MountRunningSha256: req.MountRunningSha256(),
|
|
||||||
MountIsExternalPlugin: req.MountIsExternalPlugin(),
|
|
||||||
MountClass: req.MountClass(),
|
|
||||||
Namespace: &AuditNamespace{
|
|
||||||
ID: ns.ID,
|
|
||||||
Path: ns.Path,
|
|
||||||
},
|
|
||||||
Path: req.Path,
|
|
||||||
Data: req.Data,
|
|
||||||
PolicyOverride: req.PolicyOverride,
|
|
||||||
RemoteAddr: getRemoteAddr(req),
|
|
||||||
RemotePort: getRemotePort(req),
|
|
||||||
ClientCertificateSerialNumber: getClientCertificateSerialNumber(connState),
|
|
||||||
ReplicationCluster: req.ReplicationCluster,
|
|
||||||
Headers: req.Headers,
|
|
||||||
},
|
|
||||||
|
|
||||||
Response: &AuditResponse{
|
|
||||||
MountPoint: req.MountPoint,
|
|
||||||
MountType: req.MountType,
|
|
||||||
MountAccessor: req.MountAccessor,
|
|
||||||
MountRunningVersion: req.MountRunningVersion(),
|
|
||||||
MountRunningSha256: req.MountRunningSha256(),
|
|
||||||
MountIsExternalPlugin: req.MountIsExternalPlugin(),
|
|
||||||
MountClass: req.MountClass(),
|
|
||||||
Auth: respAuth,
|
|
||||||
Secret: respSecret,
|
|
||||||
Data: respData,
|
|
||||||
Warnings: resp.Warnings,
|
|
||||||
Redirect: resp.Redirect,
|
|
||||||
WrapInfo: respWrapInfo,
|
|
||||||
Headers: resp.Headers,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
if auth.PolicyResults != nil {
|
|
||||||
respEntry.Auth.PolicyResults = &AuditPolicyResults{
|
|
||||||
Allowed: auth.PolicyResults.Allowed,
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range auth.PolicyResults.GrantingPolicies {
|
|
||||||
respEntry.Auth.PolicyResults.GrantingPolicies = append(respEntry.Auth.PolicyResults.GrantingPolicies, PolicyInfo{
|
|
||||||
Name: p.Name,
|
|
||||||
NamespaceId: p.NamespaceId,
|
|
||||||
NamespacePath: p.NamespacePath,
|
|
||||||
Type: p.Type,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !auth.IssueTime.IsZero() {
|
|
||||||
respEntry.Auth.TokenIssueTime = auth.IssueTime.Format(time.RFC3339)
|
|
||||||
}
|
|
||||||
if req.WrapInfo != nil {
|
|
||||||
respEntry.Request.WrapTTL = int(req.WrapInfo.TTL / time.Second)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !config.OmitTime {
|
|
||||||
respEntry.Time = time.Now().UTC().Format(time.RFC3339Nano)
|
|
||||||
}
|
|
||||||
|
|
||||||
return respEntry, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatAndWriteRequest attempts to format the specified logical.LogInput into an AuditRequestEntry,
|
|
||||||
// and then write the request using the specified io.Writer.
|
|
||||||
func (f *AuditFormatterWriter) FormatAndWriteRequest(ctx context.Context, w io.Writer, config FormatterConfig, in *logical.LogInput) error {
|
|
||||||
switch {
|
|
||||||
case in == nil || in.Request == nil:
|
|
||||||
return fmt.Errorf("request to request-audit a nil request")
|
|
||||||
case w == nil:
|
|
||||||
return fmt.Errorf("writer for audit request is nil")
|
|
||||||
case f.Formatter == nil:
|
|
||||||
return fmt.Errorf("no formatter specifed")
|
|
||||||
case f.Writer == nil:
|
|
||||||
return fmt.Errorf("no writer specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
reqEntry, err := f.Formatter.FormatRequest(ctx, config, in)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return f.Writer.WriteRequest(w, reqEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FormatAndWriteResponse attempts to format the specified logical.LogInput into an AuditResponseEntry,
|
|
||||||
// and then write the response using the specified io.Writer.
|
|
||||||
func (f *AuditFormatterWriter) FormatAndWriteResponse(ctx context.Context, w io.Writer, config FormatterConfig, in *logical.LogInput) error {
|
|
||||||
switch {
|
|
||||||
case in == nil || in.Request == nil:
|
|
||||||
return errors.New("request to response-audit a nil request")
|
|
||||||
case w == nil:
|
|
||||||
return errors.New("writer for audit request is nil")
|
|
||||||
case f.Formatter == nil:
|
|
||||||
return errors.New("no formatter specified")
|
|
||||||
case f.Writer == nil:
|
|
||||||
return errors.New("no writer specified")
|
|
||||||
}
|
|
||||||
|
|
||||||
respEntry, err := f.FormatResponse(ctx, config, in)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return f.Writer.WriteResponse(w, respEntry)
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuditRequestEntry is the structure of a request audit log entry in Audit.
|
|
||||||
type AuditRequestEntry struct {
|
|
||||||
Time string `json:"time,omitempty"`
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
Auth *AuditAuth `json:"auth,omitempty"`
|
|
||||||
Request *AuditRequest `json:"request,omitempty"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
ForwardedFrom string `json:"forwarded_from,omitempty"` // Populated in Enterprise when a request is forwarded
|
|
||||||
}
|
|
||||||
|
|
||||||
// AuditResponseEntry is the structure of a response audit log entry in Audit.
|
|
||||||
type AuditResponseEntry struct {
|
|
||||||
Time string `json:"time,omitempty"`
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
Auth *AuditAuth `json:"auth,omitempty"`
|
|
||||||
Request *AuditRequest `json:"request,omitempty"`
|
|
||||||
Response *AuditResponse `json:"response,omitempty"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
Forwarded bool `json:"forwarded,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuditRequest struct {
|
|
||||||
ID string `json:"id,omitempty"`
|
|
||||||
ClientID string `json:"client_id,omitempty"`
|
|
||||||
ReplicationCluster string `json:"replication_cluster,omitempty"`
|
|
||||||
Operation logical.Operation `json:"operation,omitempty"`
|
|
||||||
MountPoint string `json:"mount_point,omitempty"`
|
|
||||||
MountType string `json:"mount_type,omitempty"`
|
|
||||||
MountAccessor string `json:"mount_accessor,omitempty"`
|
|
||||||
MountRunningVersion string `json:"mount_running_version,omitempty"`
|
|
||||||
MountRunningSha256 string `json:"mount_running_sha256,omitempty"`
|
|
||||||
MountClass string `json:"mount_class,omitempty"`
|
|
||||||
MountIsExternalPlugin bool `json:"mount_is_external_plugin,omitempty"`
|
|
||||||
ClientToken string `json:"client_token,omitempty"`
|
|
||||||
ClientTokenAccessor string `json:"client_token_accessor,omitempty"`
|
|
||||||
Namespace *AuditNamespace `json:"namespace,omitempty"`
|
|
||||||
Path string `json:"path,omitempty"`
|
|
||||||
Data map[string]interface{} `json:"data,omitempty"`
|
|
||||||
PolicyOverride bool `json:"policy_override,omitempty"`
|
|
||||||
RemoteAddr string `json:"remote_address,omitempty"`
|
|
||||||
RemotePort int `json:"remote_port,omitempty"`
|
|
||||||
WrapTTL int `json:"wrap_ttl,omitempty"`
|
|
||||||
Headers map[string][]string `json:"headers,omitempty"`
|
|
||||||
ClientCertificateSerialNumber string `json:"client_certificate_serial_number,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuditResponse struct {
|
|
||||||
Auth *AuditAuth `json:"auth,omitempty"`
|
|
||||||
MountPoint string `json:"mount_point,omitempty"`
|
|
||||||
MountType string `json:"mount_type,omitempty"`
|
|
||||||
MountAccessor string `json:"mount_accessor,omitempty"`
|
|
||||||
MountRunningVersion string `json:"mount_running_plugin_version,omitempty"`
|
|
||||||
MountRunningSha256 string `json:"mount_running_sha256,omitempty"`
|
|
||||||
MountClass string `json:"mount_class,omitempty"`
|
|
||||||
MountIsExternalPlugin bool `json:"mount_is_external_plugin,omitempty"`
|
|
||||||
Secret *AuditSecret `json:"secret,omitempty"`
|
|
||||||
Data map[string]interface{} `json:"data,omitempty"`
|
|
||||||
Warnings []string `json:"warnings,omitempty"`
|
|
||||||
Redirect string `json:"redirect,omitempty"`
|
|
||||||
WrapInfo *AuditResponseWrapInfo `json:"wrap_info,omitempty"`
|
|
||||||
Headers map[string][]string `json:"headers,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuditAuth struct {
|
|
||||||
ClientToken string `json:"client_token,omitempty"`
|
|
||||||
Accessor string `json:"accessor,omitempty"`
|
|
||||||
DisplayName string `json:"display_name,omitempty"`
|
|
||||||
Policies []string `json:"policies,omitempty"`
|
|
||||||
TokenPolicies []string `json:"token_policies,omitempty"`
|
|
||||||
IdentityPolicies []string `json:"identity_policies,omitempty"`
|
|
||||||
ExternalNamespacePolicies map[string][]string `json:"external_namespace_policies,omitempty"`
|
|
||||||
NoDefaultPolicy bool `json:"no_default_policy,omitempty"`
|
|
||||||
PolicyResults *AuditPolicyResults `json:"policy_results,omitempty"`
|
|
||||||
Metadata map[string]string `json:"metadata,omitempty"`
|
|
||||||
NumUses int `json:"num_uses,omitempty"`
|
|
||||||
RemainingUses int `json:"remaining_uses,omitempty"`
|
|
||||||
EntityID string `json:"entity_id,omitempty"`
|
|
||||||
EntityCreated bool `json:"entity_created,omitempty"`
|
|
||||||
TokenType string `json:"token_type,omitempty"`
|
|
||||||
TokenTTL int64 `json:"token_ttl,omitempty"`
|
|
||||||
TokenIssueTime string `json:"token_issue_time,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuditPolicyResults struct {
|
|
||||||
Allowed bool `json:"allowed"`
|
|
||||||
GrantingPolicies []PolicyInfo `json:"granting_policies,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PolicyInfo struct {
|
|
||||||
Name string `json:"name,omitempty"`
|
|
||||||
NamespaceId string `json:"namespace_id,omitempty"`
|
|
||||||
NamespacePath string `json:"namespace_path,omitempty"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuditSecret struct {
|
|
||||||
LeaseID string `json:"lease_id,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuditResponseWrapInfo struct {
|
|
||||||
TTL int `json:"ttl,omitempty"`
|
|
||||||
Token string `json:"token,omitempty"`
|
|
||||||
Accessor string `json:"accessor,omitempty"`
|
|
||||||
CreationTime string `json:"creation_time,omitempty"`
|
|
||||||
CreationPath string `json:"creation_path,omitempty"`
|
|
||||||
WrappedAccessor string `json:"wrapped_accessor,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type AuditNamespace struct {
|
|
||||||
ID string `json:"id,omitempty"`
|
|
||||||
Path string `json:"path,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// getRemoteAddr safely gets the remote address avoiding a nil pointer
|
|
||||||
func getRemoteAddr(req *logical.Request) string {
|
|
||||||
if req != nil && req.Connection != nil {
|
|
||||||
return req.Connection.RemoteAddr
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// getRemotePort safely gets the remote port avoiding a nil pointer
|
|
||||||
func getRemotePort(req *logical.Request) int {
|
|
||||||
if req != nil && req.Connection != nil {
|
|
||||||
return req.Connection.RemotePort
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// getClientCertificateSerialNumber attempts the retrieve the serial number of
|
|
||||||
// the peer certificate from the specified tls.ConnectionState.
|
|
||||||
func getClientCertificateSerialNumber(connState *tls.ConnectionState) string {
|
|
||||||
if connState == nil || len(connState.VerifiedChains) == 0 || len(connState.VerifiedChains[0]) == 0 {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return connState.VerifiedChains[0][0].SerialNumber.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseVaultTokenFromJWT returns a string iff the token was a JWT and we could
|
|
||||||
// extract the original token ID from inside
|
|
||||||
func parseVaultTokenFromJWT(token string) *string {
|
|
||||||
if strings.Count(token, ".") != 2 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
parsedJWT, err := jwt.ParseSigned(token)
|
|
||||||
if err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var claims jwt.Claims
|
|
||||||
if err = parsedJWT.UnsafeClaimsWithoutVerification(&claims); err != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return &claims.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewTemporaryFormatter creates a formatter not backed by a persistent salt
|
|
||||||
func NewTemporaryFormatter(format, prefix string) *AuditFormatterWriter {
|
|
||||||
// We can ignore the error from NewAuditFormatter since we are sure the salter isn't nil.
|
|
||||||
f, _ := NewAuditFormatter(&nonPersistentSalt{})
|
|
||||||
|
|
||||||
var w Writer
|
|
||||||
|
|
||||||
switch format {
|
|
||||||
case "jsonx":
|
|
||||||
w = &JSONxWriter{Prefix: prefix}
|
|
||||||
default:
|
|
||||||
w = &JSONWriter{Prefix: prefix}
|
|
||||||
}
|
|
||||||
|
|
||||||
// We can ignore the error from NewAuditFormatterWriter since we are sure both
|
|
||||||
// the formatter and writer are not nil.
|
|
||||||
fw, _ := NewAuditFormatterWriter(f, w)
|
|
||||||
|
|
||||||
return fw
|
|
||||||
}
|
|
||||||
|
|
||||||
// doElideListResponseData performs the actual elision of list operation response data, once surrounding code has
|
|
||||||
// determined it should apply to a particular request. The data map that is passed in must be a copy that is safe to
|
|
||||||
// modify in place, but need not be a full recursive deep copy, as only top-level keys are changed.
|
|
||||||
//
|
|
||||||
// See the documentation of the controlling option in FormatterConfig for more information on the purpose.
|
|
||||||
func doElideListResponseData(data map[string]interface{}) {
|
|
||||||
for k, v := range data {
|
|
||||||
if k == "keys" {
|
|
||||||
if vSlice, ok := v.([]string); ok {
|
|
||||||
data[k] = len(vSlice)
|
|
||||||
}
|
|
||||||
} else if k == "key_info" {
|
|
||||||
if vMap, ok := v.(map[string]interface{}); ok {
|
|
||||||
data[k] = len(vMap)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
166
audit/options.go
Normal file
166
audit/options.go
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package audit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Option is how options are passed as arguments.
|
||||||
|
type Option func(*options) error
|
||||||
|
|
||||||
|
// options are used to represent configuration for a audit related nodes.
|
||||||
|
type options struct {
|
||||||
|
withID string
|
||||||
|
withNow time.Time
|
||||||
|
withSubtype subtype
|
||||||
|
withFormat format
|
||||||
|
withPrefix string
|
||||||
|
withRaw bool
|
||||||
|
withElision bool
|
||||||
|
withOmitTime bool
|
||||||
|
withHMACAccessor bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// getDefaultOptions returns options with their default values.
|
||||||
|
func getDefaultOptions() options {
|
||||||
|
return options{
|
||||||
|
withNow: time.Now(),
|
||||||
|
withFormat: JSONFormat,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// getOpts applies each supplied Option and returns the fully configured options.
|
||||||
|
// Each Option is applied in the order it appears in the argument list, so it is
|
||||||
|
// possible to supply the same Option numerous times and the 'last write wins'.
|
||||||
|
func getOpts(opt ...Option) (options, error) {
|
||||||
|
opts := getDefaultOptions()
|
||||||
|
for _, o := range opt {
|
||||||
|
if o == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if err := o(&opts); err != nil {
|
||||||
|
return options{}, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return opts, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithID provides an optional ID.
|
||||||
|
func WithID(id string) Option {
|
||||||
|
return func(o *options) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
id := strings.TrimSpace(id)
|
||||||
|
switch {
|
||||||
|
case id == "":
|
||||||
|
err = errors.New("id cannot be empty")
|
||||||
|
default:
|
||||||
|
o.withID = id
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithNow provides an Option to represent 'now'.
|
||||||
|
func WithNow(now time.Time) Option {
|
||||||
|
return func(o *options) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case now.IsZero():
|
||||||
|
err = errors.New("cannot specify 'now' to be the zero time instant")
|
||||||
|
default:
|
||||||
|
o.withNow = now
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithSubtype provides an Option to represent the event subtype.
|
||||||
|
func WithSubtype(s string) Option {
|
||||||
|
return func(o *options) error {
|
||||||
|
s := strings.TrimSpace(s)
|
||||||
|
if s == "" {
|
||||||
|
return errors.New("subtype cannot be empty")
|
||||||
|
}
|
||||||
|
parsed := subtype(s)
|
||||||
|
err := parsed.validate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.withSubtype = parsed
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFormat provides an Option to represent event format.
|
||||||
|
func WithFormat(f string) Option {
|
||||||
|
return func(o *options) error {
|
||||||
|
f := strings.TrimSpace(f)
|
||||||
|
if f == "" {
|
||||||
|
// Return early, we won't attempt to apply this option if its empty.
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed := format(f)
|
||||||
|
err := parsed.validate()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.withFormat = parsed
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithPrefix provides an Option to represent a prefix for a file sink.
|
||||||
|
func WithPrefix(prefix string) Option {
|
||||||
|
return func(o *options) error {
|
||||||
|
prefix = strings.TrimSpace(prefix)
|
||||||
|
|
||||||
|
if prefix != "" {
|
||||||
|
o.withPrefix = prefix
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithRaw provides an Option to represent whether 'raw' is required.
|
||||||
|
func WithRaw(r bool) Option {
|
||||||
|
return func(o *options) error {
|
||||||
|
o.withRaw = r
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithElision provides an Option to represent whether elision (...) is required.
|
||||||
|
func WithElision(e bool) Option {
|
||||||
|
return func(o *options) error {
|
||||||
|
o.withElision = e
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithOmitTime provides an Option to represent whether to omit time.
|
||||||
|
func WithOmitTime(t bool) Option {
|
||||||
|
return func(o *options) error {
|
||||||
|
o.withOmitTime = t
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithHMACAccessor provides an Option to represent whether an HMAC accessor is applicable.
|
||||||
|
func WithHMACAccessor(h bool) Option {
|
||||||
|
return func(o *options) error {
|
||||||
|
o.withHMACAccessor = h
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
487
audit/options_test.go
Normal file
487
audit/options_test.go
Normal file
@@ -0,0 +1,487 @@
|
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package audit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestOptions_WithFormat exercises WithFormat Option to ensure it performs as expected.
|
||||||
|
func TestOptions_WithFormat(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
Value string
|
||||||
|
IsErrorExpected bool
|
||||||
|
ExpectedErrorMessage string
|
||||||
|
ExpectedValue format
|
||||||
|
}{
|
||||||
|
"empty": {
|
||||||
|
Value: "",
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedValue: format(""),
|
||||||
|
},
|
||||||
|
"whitespace": {
|
||||||
|
Value: " ",
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedValue: format(""),
|
||||||
|
},
|
||||||
|
"invalid-test": {
|
||||||
|
Value: "test",
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "audit.(format).validate: 'test' is not a valid format: invalid parameter",
|
||||||
|
},
|
||||||
|
"valid-json": {
|
||||||
|
Value: "json",
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedValue: JSONFormat,
|
||||||
|
},
|
||||||
|
"valid-jsonx": {
|
||||||
|
Value: "jsonx",
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedValue: JSONxFormat,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
options := &options{}
|
||||||
|
applyOption := WithFormat(tc.Value)
|
||||||
|
err := applyOption(options)
|
||||||
|
switch {
|
||||||
|
case tc.IsErrorExpected:
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
||||||
|
default:
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.ExpectedValue, options.withFormat)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOptions_WithSubtype exercises WithSubtype Option to ensure it performs as expected.
|
||||||
|
func TestOptions_WithSubtype(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
Value string
|
||||||
|
IsErrorExpected bool
|
||||||
|
ExpectedErrorMessage string
|
||||||
|
ExpectedValue subtype
|
||||||
|
}{
|
||||||
|
"empty": {
|
||||||
|
Value: "",
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "subtype cannot be empty",
|
||||||
|
},
|
||||||
|
"whitespace": {
|
||||||
|
Value: " ",
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "subtype cannot be empty",
|
||||||
|
},
|
||||||
|
"valid": {
|
||||||
|
Value: "AuditResponse",
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedValue: ResponseType,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
options := &options{}
|
||||||
|
applyOption := WithSubtype(tc.Value)
|
||||||
|
err := applyOption(options)
|
||||||
|
switch {
|
||||||
|
case tc.IsErrorExpected:
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
||||||
|
default:
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.ExpectedValue, options.withSubtype)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOptions_WithNow exercises WithNow Option to ensure it performs as expected.
|
||||||
|
func TestOptions_WithNow(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
Value time.Time
|
||||||
|
IsErrorExpected bool
|
||||||
|
ExpectedErrorMessage string
|
||||||
|
ExpectedValue time.Time
|
||||||
|
}{
|
||||||
|
"default-time": {
|
||||||
|
Value: time.Time{},
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "cannot specify 'now' to be the zero time instant",
|
||||||
|
},
|
||||||
|
"valid-time": {
|
||||||
|
Value: time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local),
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedValue: time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
options := &options{}
|
||||||
|
applyOption := WithNow(tc.Value)
|
||||||
|
err := applyOption(options)
|
||||||
|
switch {
|
||||||
|
case tc.IsErrorExpected:
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
||||||
|
default:
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.ExpectedValue, options.withNow)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOptions_WithID exercises WithID Option to ensure it performs as expected.
|
||||||
|
func TestOptions_WithID(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
Value string
|
||||||
|
IsErrorExpected bool
|
||||||
|
ExpectedErrorMessage string
|
||||||
|
ExpectedValue string
|
||||||
|
}{
|
||||||
|
"empty": {
|
||||||
|
Value: "",
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "id cannot be empty",
|
||||||
|
},
|
||||||
|
"whitespace": {
|
||||||
|
Value: " ",
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "id cannot be empty",
|
||||||
|
},
|
||||||
|
"valid": {
|
||||||
|
Value: "test",
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedValue: "test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
options := &options{}
|
||||||
|
applyOption := WithID(tc.Value)
|
||||||
|
err := applyOption(options)
|
||||||
|
switch {
|
||||||
|
case tc.IsErrorExpected:
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
||||||
|
default:
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.ExpectedValue, options.withID)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOptions_WithPrefix exercises WithPrefix Option to ensure it performs as expected.
|
||||||
|
func TestOptions_WithPrefix(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
Value string
|
||||||
|
IsErrorExpected bool
|
||||||
|
ExpectedErrorMessage string
|
||||||
|
ExpectedValue string
|
||||||
|
}{
|
||||||
|
"empty": {
|
||||||
|
Value: "",
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedValue: "",
|
||||||
|
},
|
||||||
|
"whitespace": {
|
||||||
|
Value: " ",
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedErrorMessage: "",
|
||||||
|
},
|
||||||
|
"valid": {
|
||||||
|
Value: "test",
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedValue: "test",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
options := &options{}
|
||||||
|
applyOption := WithPrefix(tc.Value)
|
||||||
|
err := applyOption(options)
|
||||||
|
switch {
|
||||||
|
case tc.IsErrorExpected:
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
||||||
|
default:
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.ExpectedValue, options.withPrefix)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOptions_WithRaw exercises WithRaw Option to ensure it performs as expected.
|
||||||
|
func TestOptions_WithRaw(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
Value bool
|
||||||
|
ExpectedValue bool
|
||||||
|
}{
|
||||||
|
"true": {
|
||||||
|
Value: true,
|
||||||
|
ExpectedValue: true,
|
||||||
|
},
|
||||||
|
"false": {
|
||||||
|
Value: false,
|
||||||
|
ExpectedValue: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
options := &options{}
|
||||||
|
applyOption := WithRaw(tc.Value)
|
||||||
|
err := applyOption(options)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.ExpectedValue, options.withRaw)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOptions_WithElision exercises WithElision Option to ensure it performs as expected.
|
||||||
|
func TestOptions_WithElision(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
Value bool
|
||||||
|
ExpectedValue bool
|
||||||
|
}{
|
||||||
|
"true": {
|
||||||
|
Value: true,
|
||||||
|
ExpectedValue: true,
|
||||||
|
},
|
||||||
|
"false": {
|
||||||
|
Value: false,
|
||||||
|
ExpectedValue: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
options := &options{}
|
||||||
|
applyOption := WithElision(tc.Value)
|
||||||
|
err := applyOption(options)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.ExpectedValue, options.withElision)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOptions_WithHMACAccessor exercises WithHMACAccessor Option to ensure it performs as expected.
|
||||||
|
func TestOptions_WithHMACAccessor(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
Value bool
|
||||||
|
ExpectedValue bool
|
||||||
|
}{
|
||||||
|
"true": {
|
||||||
|
Value: true,
|
||||||
|
ExpectedValue: true,
|
||||||
|
},
|
||||||
|
"false": {
|
||||||
|
Value: false,
|
||||||
|
ExpectedValue: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
options := &options{}
|
||||||
|
applyOption := WithHMACAccessor(tc.Value)
|
||||||
|
err := applyOption(options)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.ExpectedValue, options.withHMACAccessor)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOptions_WithOmitTime exercises WithOmitTime Option to ensure it performs as expected.
|
||||||
|
func TestOptions_WithOmitTime(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
Value bool
|
||||||
|
ExpectedValue bool
|
||||||
|
}{
|
||||||
|
"true": {
|
||||||
|
Value: true,
|
||||||
|
ExpectedValue: true,
|
||||||
|
},
|
||||||
|
"false": {
|
||||||
|
Value: false,
|
||||||
|
ExpectedValue: false,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
options := &options{}
|
||||||
|
applyOption := WithOmitTime(tc.Value)
|
||||||
|
err := applyOption(options)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.ExpectedValue, options.withOmitTime)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOptions_Default exercises getDefaultOptions to assert the default values.
|
||||||
|
func TestOptions_Default(t *testing.T) {
|
||||||
|
opts := getDefaultOptions()
|
||||||
|
require.NotNil(t, opts)
|
||||||
|
require.True(t, time.Now().After(opts.withNow))
|
||||||
|
require.False(t, opts.withNow.IsZero())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOptions_Opts exercises GetOpts with various Option values.
|
||||||
|
func TestOptions_Opts(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
opts []Option
|
||||||
|
IsErrorExpected bool
|
||||||
|
ExpectedErrorMessage string
|
||||||
|
ExpectedID string
|
||||||
|
ExpectedSubtype subtype
|
||||||
|
ExpectedFormat format
|
||||||
|
IsNowExpected bool
|
||||||
|
ExpectedNow time.Time
|
||||||
|
}{
|
||||||
|
"nil-options": {
|
||||||
|
opts: nil,
|
||||||
|
IsErrorExpected: false,
|
||||||
|
IsNowExpected: true,
|
||||||
|
ExpectedFormat: JSONFormat,
|
||||||
|
},
|
||||||
|
"empty-options": {
|
||||||
|
opts: []Option{},
|
||||||
|
IsErrorExpected: false,
|
||||||
|
IsNowExpected: true,
|
||||||
|
ExpectedFormat: JSONFormat,
|
||||||
|
},
|
||||||
|
"with-multiple-valid-id": {
|
||||||
|
opts: []Option{
|
||||||
|
WithID("qwerty"),
|
||||||
|
WithID("juan"),
|
||||||
|
},
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedID: "juan",
|
||||||
|
IsNowExpected: true,
|
||||||
|
ExpectedFormat: JSONFormat,
|
||||||
|
},
|
||||||
|
"with-multiple-valid-subtype": {
|
||||||
|
opts: []Option{
|
||||||
|
WithSubtype("AuditRequest"),
|
||||||
|
WithSubtype("AuditResponse"),
|
||||||
|
},
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedSubtype: ResponseType,
|
||||||
|
IsNowExpected: true,
|
||||||
|
ExpectedFormat: JSONFormat,
|
||||||
|
},
|
||||||
|
"with-multiple-valid-format": {
|
||||||
|
opts: []Option{
|
||||||
|
WithFormat("json"),
|
||||||
|
WithFormat("jsonx"),
|
||||||
|
},
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedFormat: JSONxFormat,
|
||||||
|
IsNowExpected: true,
|
||||||
|
},
|
||||||
|
"with-multiple-valid-now": {
|
||||||
|
opts: []Option{
|
||||||
|
WithNow(time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local)),
|
||||||
|
WithNow(time.Date(2023, time.July, 4, 13, 3, 0, 0, time.Local)),
|
||||||
|
},
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedNow: time.Date(2023, time.July, 4, 13, 3, 0, 0, time.Local),
|
||||||
|
IsNowExpected: false,
|
||||||
|
ExpectedFormat: JSONFormat,
|
||||||
|
},
|
||||||
|
"with-multiple-valid-then-invalid-now": {
|
||||||
|
opts: []Option{
|
||||||
|
WithNow(time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local)),
|
||||||
|
WithNow(time.Time{}),
|
||||||
|
},
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "cannot specify 'now' to be the zero time instant",
|
||||||
|
ExpectedFormat: JSONFormat,
|
||||||
|
},
|
||||||
|
"with-multiple-valid-options": {
|
||||||
|
opts: []Option{
|
||||||
|
WithID("qwerty"),
|
||||||
|
WithSubtype("AuditRequest"),
|
||||||
|
WithFormat("json"),
|
||||||
|
WithNow(time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local)),
|
||||||
|
},
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedID: "qwerty",
|
||||||
|
ExpectedSubtype: RequestType,
|
||||||
|
ExpectedFormat: JSONFormat,
|
||||||
|
ExpectedNow: time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
opts, err := getOpts(tc.opts...)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case tc.IsErrorExpected:
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
||||||
|
default:
|
||||||
|
require.NotNil(t, opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.ExpectedID, opts.withID)
|
||||||
|
require.Equal(t, tc.ExpectedSubtype, opts.withSubtype)
|
||||||
|
require.Equal(t, tc.ExpectedFormat, opts.withFormat)
|
||||||
|
switch {
|
||||||
|
case tc.IsNowExpected:
|
||||||
|
require.True(t, time.Now().After(opts.withNow))
|
||||||
|
require.False(t, opts.withNow.IsZero())
|
||||||
|
default:
|
||||||
|
require.Equal(t, tc.ExpectedNow, opts.withNow)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,7 +16,7 @@ type JSONWriter struct {
|
|||||||
Prefix string
|
Prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *JSONWriter) WriteRequest(w io.Writer, req *AuditRequestEntry) error {
|
func (f *JSONWriter) WriteRequest(w io.Writer, req *RequestEntry) error {
|
||||||
if req == nil {
|
if req == nil {
|
||||||
return fmt.Errorf("request entry was nil, cannot encode")
|
return fmt.Errorf("request entry was nil, cannot encode")
|
||||||
}
|
}
|
||||||
@@ -32,7 +32,7 @@ func (f *JSONWriter) WriteRequest(w io.Writer, req *AuditRequestEntry) error {
|
|||||||
return enc.Encode(req)
|
return enc.Encode(req)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *JSONWriter) WriteResponse(w io.Writer, resp *AuditResponseEntry) error {
|
func (f *JSONWriter) WriteResponse(w io.Writer, resp *ResponseEntry) error {
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
return fmt.Errorf("response entry was nil, cannot encode")
|
return fmt.Errorf("response entry was nil, cannot encode")
|
||||||
}
|
}
|
||||||
@@ -98,38 +98,39 @@ func TestFormatJSON_formatRequest(t *testing.T) {
|
|||||||
|
|
||||||
for name, tc := range cases {
|
for name, tc := range cases {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
f, err := NewAuditFormatter(ss)
|
cfg, err := NewFormatterConfig()
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
formatter := AuditFormatterWriter{
|
f, err := NewEntryFormatter(cfg, ss)
|
||||||
|
require.NoError(t, err)
|
||||||
|
formatter := EntryFormatterWriter{
|
||||||
Formatter: f,
|
Formatter: f,
|
||||||
Writer: &JSONWriter{
|
Writer: &JSONWriter{
|
||||||
Prefix: tc.Prefix,
|
Prefix: tc.Prefix,
|
||||||
},
|
},
|
||||||
|
config: cfg,
|
||||||
}
|
}
|
||||||
config := FormatterConfig{
|
|
||||||
HMACAccessor: false,
|
|
||||||
}
|
|
||||||
in := &logical.LogInput{
|
in := &logical.LogInput{
|
||||||
Auth: tc.Auth,
|
Auth: tc.Auth,
|
||||||
Request: tc.Req,
|
Request: tc.Req,
|
||||||
OuterErr: tc.Err,
|
OuterErr: tc.Err,
|
||||||
}
|
}
|
||||||
|
|
||||||
err = formatter.FormatAndWriteRequest(namespace.RootContext(nil), &buf, config, in)
|
err = formatter.FormatAndWriteRequest(namespace.RootContext(nil), &buf, in)
|
||||||
require.NoErrorf(t, err, "bad: %s\nerr: %s", name, err)
|
require.NoErrorf(t, err, "bad: %s\nerr: %s", name, err)
|
||||||
|
|
||||||
if !strings.HasPrefix(buf.String(), tc.Prefix) {
|
if !strings.HasPrefix(buf.String(), tc.Prefix) {
|
||||||
t.Fatalf("no prefix: %s \n log: %s\nprefix: %s", name, expectedResultStr, tc.Prefix)
|
t.Fatalf("no prefix: %s \n log: %s\nprefix: %s", name, expectedResultStr, tc.Prefix)
|
||||||
}
|
}
|
||||||
|
|
||||||
expectedJSON := new(AuditRequestEntry)
|
expectedJSON := new(RequestEntry)
|
||||||
|
|
||||||
if err := jsonutil.DecodeJSON([]byte(expectedResultStr), &expectedJSON); err != nil {
|
if err := jsonutil.DecodeJSON([]byte(expectedResultStr), &expectedJSON); err != nil {
|
||||||
t.Fatalf("bad json: %s", err)
|
t.Fatalf("bad json: %s", err)
|
||||||
}
|
}
|
||||||
expectedJSON.Request.Namespace = &AuditNamespace{ID: "root"}
|
expectedJSON.Request.Namespace = &Namespace{ID: "root"}
|
||||||
|
|
||||||
actualjson := new(AuditRequestEntry)
|
actualjson := new(RequestEntry)
|
||||||
if err := jsonutil.DecodeJSON([]byte(buf.String())[len(tc.Prefix):], &actualjson); err != nil {
|
if err := jsonutil.DecodeJSON([]byte(buf.String())[len(tc.Prefix):], &actualjson); err != nil {
|
||||||
t.Fatalf("bad json: %s", err)
|
t.Fatalf("bad json: %s", err)
|
||||||
}
|
}
|
||||||
@@ -18,7 +18,7 @@ type JSONxWriter struct {
|
|||||||
Prefix string
|
Prefix string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *JSONxWriter) WriteRequest(w io.Writer, req *AuditRequestEntry) error {
|
func (f *JSONxWriter) WriteRequest(w io.Writer, req *RequestEntry) error {
|
||||||
if req == nil {
|
if req == nil {
|
||||||
return fmt.Errorf("request entry was nil, cannot encode")
|
return fmt.Errorf("request entry was nil, cannot encode")
|
||||||
}
|
}
|
||||||
@@ -44,7 +44,7 @@ func (f *JSONxWriter) WriteRequest(w io.Writer, req *AuditRequestEntry) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *JSONxWriter) WriteResponse(w io.Writer, resp *AuditResponseEntry) error {
|
func (f *JSONxWriter) WriteResponse(w io.Writer, resp *ResponseEntry) error {
|
||||||
if resp == nil {
|
if resp == nil {
|
||||||
return fmt.Errorf("response entry was nil, cannot encode")
|
return fmt.Errorf("response entry was nil, cannot encode")
|
||||||
}
|
}
|
||||||
@@ -113,24 +113,25 @@ func TestFormatJSONx_formatRequest(t *testing.T) {
|
|||||||
|
|
||||||
for name, tc := range cases {
|
for name, tc := range cases {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
f, err := NewAuditFormatter(tempStaticSalt)
|
cfg, err := NewFormatterConfig(
|
||||||
|
WithOmitTime(true),
|
||||||
|
WithHMACAccessor(false),
|
||||||
|
WithFormat(JSONxFormat.String()),
|
||||||
|
)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
formatter := AuditFormatterWriter{
|
f, err := NewEntryFormatter(cfg, tempStaticSalt)
|
||||||
Formatter: f,
|
require.NoError(t, err)
|
||||||
Writer: &JSONxWriter{
|
writer := &JSONxWriter{Prefix: tc.Prefix}
|
||||||
Prefix: tc.Prefix,
|
formatter, err := NewEntryFormatterWriter(cfg, f, writer)
|
||||||
},
|
require.NoError(t, err)
|
||||||
}
|
require.NotNil(t, formatter)
|
||||||
config := FormatterConfig{
|
|
||||||
OmitTime: true,
|
|
||||||
HMACAccessor: false,
|
|
||||||
}
|
|
||||||
in := &logical.LogInput{
|
in := &logical.LogInput{
|
||||||
Auth: tc.Auth,
|
Auth: tc.Auth,
|
||||||
Request: tc.Req,
|
Request: tc.Req,
|
||||||
OuterErr: tc.Err,
|
OuterErr: tc.Err,
|
||||||
}
|
}
|
||||||
if err := formatter.FormatAndWriteRequest(namespace.RootContext(nil), &buf, config, in); err != nil {
|
if err := formatter.FormatAndWriteRequest(namespace.RootContext(nil), &buf, in); err != nil {
|
||||||
t.Fatalf("bad: %s\nerr: %s", name, err)
|
t.Fatalf("bad: %s\nerr: %s", name, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -107,17 +107,23 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg, err := audit.NewFormatterConfig(
|
||||||
|
audit.WithElision(elideListResponses),
|
||||||
|
audit.WithFormat(format),
|
||||||
|
audit.WithHMACAccessor(hmacAccessor),
|
||||||
|
audit.WithRaw(logRaw),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
b := &Backend{
|
b := &Backend{
|
||||||
path: path,
|
path: path,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
saltConfig: conf.SaltConfig,
|
saltConfig: conf.SaltConfig,
|
||||||
saltView: conf.SaltView,
|
saltView: conf.SaltView,
|
||||||
salt: new(atomic.Value),
|
salt: new(atomic.Value),
|
||||||
formatConfig: audit.FormatterConfig{
|
formatConfig: cfg,
|
||||||
Raw: logRaw,
|
|
||||||
HMACAccessor: hmacAccessor,
|
|
||||||
ElideListResponses: elideListResponses,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure we are working with the right type by explicitly storing a nil of
|
// Ensure we are working with the right type by explicitly storing a nil of
|
||||||
@@ -125,7 +131,7 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
|
|||||||
b.salt.Store((*salt.Salt)(nil))
|
b.salt.Store((*salt.Salt)(nil))
|
||||||
|
|
||||||
// Configure the formatter for either case.
|
// Configure the formatter for either case.
|
||||||
f, err := audit.NewAuditFormatter(b)
|
f, err := audit.NewEntryFormatter(b.formatConfig, b, audit.WithPrefix(conf.Config["prefix"]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating formatter: %w", err)
|
return nil, fmt.Errorf("error creating formatter: %w", err)
|
||||||
}
|
}
|
||||||
@@ -137,7 +143,7 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
|
|||||||
w = &audit.JSONxWriter{Prefix: conf.Config["prefix"]}
|
w = &audit.JSONxWriter{Prefix: conf.Config["prefix"]}
|
||||||
}
|
}
|
||||||
|
|
||||||
fw, err := audit.NewAuditFormatterWriter(f, w)
|
fw, err := audit.NewEntryFormatterWriter(b.formatConfig, f, w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating formatter writer: %w", err)
|
return nil, fmt.Errorf("error creating formatter writer: %w", err)
|
||||||
}
|
}
|
||||||
@@ -166,7 +172,7 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
|
|||||||
type Backend struct {
|
type Backend struct {
|
||||||
path string
|
path string
|
||||||
|
|
||||||
formatter *audit.AuditFormatterWriter
|
formatter *audit.EntryFormatterWriter
|
||||||
formatConfig audit.FormatterConfig
|
formatConfig audit.FormatterConfig
|
||||||
|
|
||||||
fileLock sync.RWMutex
|
fileLock sync.RWMutex
|
||||||
@@ -224,7 +230,7 @@ func (b *Backend) LogRequest(ctx context.Context, in *logical.LogInput) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buf := bytes.NewBuffer(make([]byte, 0, 2000))
|
buf := bytes.NewBuffer(make([]byte, 0, 2000))
|
||||||
err := b.formatter.FormatAndWriteRequest(ctx, buf, b.formatConfig, in)
|
err := b.formatter.FormatAndWriteRequest(ctx, buf, in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -280,7 +286,7 @@ func (b *Backend) LogResponse(ctx context.Context, in *logical.LogInput) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buf := bytes.NewBuffer(make([]byte, 0, 6000))
|
buf := bytes.NewBuffer(make([]byte, 0, 6000))
|
||||||
err := b.formatter.FormatAndWriteResponse(ctx, buf, b.formatConfig, in)
|
err := b.formatter.FormatAndWriteResponse(ctx, buf, in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -298,8 +304,12 @@ func (b *Backend) LogTestMessage(ctx context.Context, in *logical.LogInput, conf
|
|||||||
}
|
}
|
||||||
|
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
temporaryFormatter := audit.NewTemporaryFormatter(config["format"], config["prefix"])
|
temporaryFormatter, err := audit.NewTemporaryFormatter(config["format"], config["prefix"])
|
||||||
if err := temporaryFormatter.FormatAndWriteRequest(ctx, &buf, b.formatConfig, in); err != nil {
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = temporaryFormatter.FormatAndWriteRequest(ctx, &buf, in); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -85,14 +85,20 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
|
|||||||
elideListResponses = value
|
elideListResponses = value
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg, err := audit.NewFormatterConfig(
|
||||||
|
audit.WithElision(elideListResponses),
|
||||||
|
audit.WithFormat(format),
|
||||||
|
audit.WithHMACAccessor(hmacAccessor),
|
||||||
|
audit.WithRaw(logRaw),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
b := &Backend{
|
b := &Backend{
|
||||||
saltConfig: conf.SaltConfig,
|
saltConfig: conf.SaltConfig,
|
||||||
saltView: conf.SaltView,
|
saltView: conf.SaltView,
|
||||||
formatConfig: audit.FormatterConfig{
|
formatConfig: cfg,
|
||||||
Raw: logRaw,
|
|
||||||
HMACAccessor: hmacAccessor,
|
|
||||||
ElideListResponses: elideListResponses,
|
|
||||||
},
|
|
||||||
|
|
||||||
writeDuration: writeDuration,
|
writeDuration: writeDuration,
|
||||||
address: address,
|
address: address,
|
||||||
@@ -100,7 +106,7 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Configure the formatter for either case.
|
// Configure the formatter for either case.
|
||||||
f, err := audit.NewAuditFormatter(b)
|
f, err := audit.NewEntryFormatter(b.formatConfig, b)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating formatter: %w", err)
|
return nil, fmt.Errorf("error creating formatter: %w", err)
|
||||||
}
|
}
|
||||||
@@ -112,7 +118,7 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
|
|||||||
w = &audit.JSONxWriter{Prefix: conf.Config["prefix"]}
|
w = &audit.JSONxWriter{Prefix: conf.Config["prefix"]}
|
||||||
}
|
}
|
||||||
|
|
||||||
fw, err := audit.NewAuditFormatterWriter(f, w)
|
fw, err := audit.NewEntryFormatterWriter(b.formatConfig, f, w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating formatter writer: %w", err)
|
return nil, fmt.Errorf("error creating formatter writer: %w", err)
|
||||||
}
|
}
|
||||||
@@ -126,7 +132,7 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
|
|||||||
type Backend struct {
|
type Backend struct {
|
||||||
connection net.Conn
|
connection net.Conn
|
||||||
|
|
||||||
formatter *audit.AuditFormatterWriter
|
formatter *audit.EntryFormatterWriter
|
||||||
formatConfig audit.FormatterConfig
|
formatConfig audit.FormatterConfig
|
||||||
|
|
||||||
writeDuration time.Duration
|
writeDuration time.Duration
|
||||||
@@ -153,7 +159,7 @@ func (b *Backend) GetHash(ctx context.Context, data string) (string, error) {
|
|||||||
|
|
||||||
func (b *Backend) LogRequest(ctx context.Context, in *logical.LogInput) error {
|
func (b *Backend) LogRequest(ctx context.Context, in *logical.LogInput) error {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := b.formatter.FormatAndWriteRequest(ctx, &buf, b.formatConfig, in); err != nil {
|
if err := b.formatter.FormatAndWriteRequest(ctx, &buf, in); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -176,7 +182,7 @@ func (b *Backend) LogRequest(ctx context.Context, in *logical.LogInput) error {
|
|||||||
|
|
||||||
func (b *Backend) LogResponse(ctx context.Context, in *logical.LogInput) error {
|
func (b *Backend) LogResponse(ctx context.Context, in *logical.LogInput) error {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := b.formatter.FormatAndWriteResponse(ctx, &buf, b.formatConfig, in); err != nil {
|
if err := b.formatter.FormatAndWriteResponse(ctx, &buf, in); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,15 +205,20 @@ func (b *Backend) LogResponse(ctx context.Context, in *logical.LogInput) error {
|
|||||||
|
|
||||||
func (b *Backend) LogTestMessage(ctx context.Context, in *logical.LogInput, config map[string]string) error {
|
func (b *Backend) LogTestMessage(ctx context.Context, in *logical.LogInput, config map[string]string) error {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
temporaryFormatter := audit.NewTemporaryFormatter(config["format"], config["prefix"])
|
|
||||||
if err := temporaryFormatter.FormatAndWriteRequest(ctx, &buf, b.formatConfig, in); err != nil {
|
temporaryFormatter, err := audit.NewTemporaryFormatter(config["format"], config["prefix"])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = temporaryFormatter.FormatAndWriteRequest(ctx, &buf, in); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
b.Lock()
|
b.Lock()
|
||||||
defer b.Unlock()
|
defer b.Unlock()
|
||||||
|
|
||||||
err := b.write(ctx, buf.Bytes())
|
err = b.write(ctx, buf.Bytes())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rErr := b.reconnect(ctx)
|
rErr := b.reconnect(ctx)
|
||||||
if rErr != nil {
|
if rErr != nil {
|
||||||
|
|||||||
@@ -81,19 +81,25 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
cfg, err := audit.NewFormatterConfig(
|
||||||
|
audit.WithElision(elideListResponses),
|
||||||
|
audit.WithFormat(format),
|
||||||
|
audit.WithHMACAccessor(hmacAccessor),
|
||||||
|
audit.WithRaw(logRaw),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
b := &Backend{
|
b := &Backend{
|
||||||
logger: logger,
|
logger: logger,
|
||||||
saltConfig: conf.SaltConfig,
|
saltConfig: conf.SaltConfig,
|
||||||
saltView: conf.SaltView,
|
saltView: conf.SaltView,
|
||||||
formatConfig: audit.FormatterConfig{
|
formatConfig: cfg,
|
||||||
Raw: logRaw,
|
|
||||||
HMACAccessor: hmacAccessor,
|
|
||||||
ElideListResponses: elideListResponses,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Configure the formatter for either case.
|
// Configure the formatter for either case.
|
||||||
f, err := audit.NewAuditFormatter(b)
|
f, err := audit.NewEntryFormatter(b.formatConfig, b, audit.WithPrefix(conf.Config["prefix"]))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating formatter: %w", err)
|
return nil, fmt.Errorf("error creating formatter: %w", err)
|
||||||
}
|
}
|
||||||
@@ -106,7 +112,7 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
|
|||||||
w = &audit.JSONxWriter{Prefix: conf.Config["prefix"]}
|
w = &audit.JSONxWriter{Prefix: conf.Config["prefix"]}
|
||||||
}
|
}
|
||||||
|
|
||||||
fw, err := audit.NewAuditFormatterWriter(f, w)
|
fw, err := audit.NewEntryFormatterWriter(b.formatConfig, f, w)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating formatter writer: %w", err)
|
return nil, fmt.Errorf("error creating formatter writer: %w", err)
|
||||||
}
|
}
|
||||||
@@ -120,7 +126,7 @@ func Factory(ctx context.Context, conf *audit.BackendConfig, useEventLogger bool
|
|||||||
type Backend struct {
|
type Backend struct {
|
||||||
logger gsyslog.Syslogger
|
logger gsyslog.Syslogger
|
||||||
|
|
||||||
formatter *audit.AuditFormatterWriter
|
formatter *audit.EntryFormatterWriter
|
||||||
formatConfig audit.FormatterConfig
|
formatConfig audit.FormatterConfig
|
||||||
|
|
||||||
saltMutex sync.RWMutex
|
saltMutex sync.RWMutex
|
||||||
@@ -141,7 +147,7 @@ func (b *Backend) GetHash(ctx context.Context, data string) (string, error) {
|
|||||||
|
|
||||||
func (b *Backend) LogRequest(ctx context.Context, in *logical.LogInput) error {
|
func (b *Backend) LogRequest(ctx context.Context, in *logical.LogInput) error {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := b.formatter.FormatAndWriteRequest(ctx, &buf, b.formatConfig, in); err != nil {
|
if err := b.formatter.FormatAndWriteRequest(ctx, &buf, in); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +158,7 @@ func (b *Backend) LogRequest(ctx context.Context, in *logical.LogInput) error {
|
|||||||
|
|
||||||
func (b *Backend) LogResponse(ctx context.Context, in *logical.LogInput) error {
|
func (b *Backend) LogResponse(ctx context.Context, in *logical.LogInput) error {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
if err := b.formatter.FormatAndWriteResponse(ctx, &buf, b.formatConfig, in); err != nil {
|
if err := b.formatter.FormatAndWriteResponse(ctx, &buf, in); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -163,13 +169,18 @@ func (b *Backend) LogResponse(ctx context.Context, in *logical.LogInput) error {
|
|||||||
|
|
||||||
func (b *Backend) LogTestMessage(ctx context.Context, in *logical.LogInput, config map[string]string) error {
|
func (b *Backend) LogTestMessage(ctx context.Context, in *logical.LogInput, config map[string]string) error {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
temporaryFormatter := audit.NewTemporaryFormatter(config["format"], config["prefix"])
|
|
||||||
if err := temporaryFormatter.FormatAndWriteRequest(ctx, &buf, b.formatConfig, in); err != nil {
|
temporaryFormatter, err := audit.NewTemporaryFormatter(config["format"], config["prefix"])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = temporaryFormatter.FormatAndWriteRequest(ctx, &buf, in); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send to syslog
|
// Send to syslog
|
||||||
_, err := b.logger.Write(buf.Bytes())
|
_, err = b.logger.Write(buf.Bytes())
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/eventlogger"
|
||||||
"github.com/hashicorp/go-hclog"
|
"github.com/hashicorp/go-hclog"
|
||||||
"github.com/hashicorp/vault/audit"
|
"github.com/hashicorp/vault/audit"
|
||||||
"github.com/hashicorp/vault/builtin/credential/approle"
|
"github.com/hashicorp/vault/builtin/credential/approle"
|
||||||
@@ -246,12 +247,17 @@ func NewNoopAudit(config map[string]string) (*NoopAudit, error) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
f, err := audit.NewAuditFormatter(n)
|
cfg, err := audit.NewFormatterConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
f, err := audit.NewEntryFormatter(cfg, n)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating formatter: %w", err)
|
return nil, fmt.Errorf("error creating formatter: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
fw, err := audit.NewAuditFormatterWriter(f, &audit.JSONWriter{})
|
fw, err := audit.NewEntryFormatterWriter(cfg, f, &audit.JSONWriter{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("error creating formatter writer: %w", err)
|
return nil, fmt.Errorf("error creating formatter writer: %w", err)
|
||||||
}
|
}
|
||||||
@@ -291,7 +297,7 @@ type NoopAudit struct {
|
|||||||
RespReqNonHMACKeys [][]string
|
RespReqNonHMACKeys [][]string
|
||||||
RespErrs []error
|
RespErrs []error
|
||||||
|
|
||||||
formatter *audit.AuditFormatterWriter
|
formatter *audit.EntryFormatterWriter
|
||||||
records [][]byte
|
records [][]byte
|
||||||
l sync.RWMutex
|
l sync.RWMutex
|
||||||
salt *salt.Salt
|
salt *salt.Salt
|
||||||
@@ -303,7 +309,7 @@ func (n *NoopAudit) LogRequest(ctx context.Context, in *logical.LogInput) error
|
|||||||
defer n.l.Unlock()
|
defer n.l.Unlock()
|
||||||
if n.formatter != nil {
|
if n.formatter != nil {
|
||||||
var w bytes.Buffer
|
var w bytes.Buffer
|
||||||
err := n.formatter.FormatAndWriteRequest(ctx, &w, audit.FormatterConfig{}, in)
|
err := n.formatter.FormatAndWriteRequest(ctx, &w, in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -325,7 +331,7 @@ func (n *NoopAudit) LogResponse(ctx context.Context, in *logical.LogInput) error
|
|||||||
|
|
||||||
if n.formatter != nil {
|
if n.formatter != nil {
|
||||||
var w bytes.Buffer
|
var w bytes.Buffer
|
||||||
err := n.formatter.FormatAndWriteResponse(ctx, &w, audit.FormatterConfig{}, in)
|
err := n.formatter.FormatAndWriteResponse(ctx, &w, in)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -349,12 +355,19 @@ func (n *NoopAudit) LogTestMessage(ctx context.Context, in *logical.LogInput, co
|
|||||||
n.l.Lock()
|
n.l.Lock()
|
||||||
defer n.l.Unlock()
|
defer n.l.Unlock()
|
||||||
var w bytes.Buffer
|
var w bytes.Buffer
|
||||||
tempFormatter := audit.NewTemporaryFormatter(config["format"], config["prefix"])
|
|
||||||
err := tempFormatter.FormatAndWriteResponse(ctx, &w, audit.FormatterConfig{}, in)
|
tempFormatter, err := audit.NewTemporaryFormatter(config["format"], config["prefix"])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = tempFormatter.FormatAndWriteResponse(ctx, &w, in)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
n.records = append(n.records, w.Bytes())
|
n.records = append(n.records, w.Bytes())
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -370,32 +383,36 @@ func (n *NoopAudit) Salt(ctx context.Context) (*salt.Salt, error) {
|
|||||||
if n.salt != nil {
|
if n.salt != nil {
|
||||||
return n.salt, nil
|
return n.salt, nil
|
||||||
}
|
}
|
||||||
salt, err := salt.NewSalt(ctx, n.Config.SaltView, n.Config.SaltConfig)
|
s, err := salt.NewSalt(ctx, n.Config.SaltView, n.Config.SaltConfig)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
n.salt = salt
|
n.salt = s
|
||||||
return salt, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NoopAudit) GetHash(ctx context.Context, data string) (string, error) {
|
func (n *NoopAudit) GetHash(ctx context.Context, data string) (string, error) {
|
||||||
salt, err := n.Salt(ctx)
|
s, err := n.Salt(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
return salt.GetIdentifiedHMAC(data), nil
|
return s.GetIdentifiedHMAC(data), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NoopAudit) Reload(ctx context.Context) error {
|
func (n *NoopAudit) Reload(_ context.Context) error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NoopAudit) Invalidate(ctx context.Context) {
|
func (n *NoopAudit) Invalidate(_ context.Context) {
|
||||||
n.saltMutex.Lock()
|
n.saltMutex.Lock()
|
||||||
defer n.saltMutex.Unlock()
|
defer n.saltMutex.Unlock()
|
||||||
n.salt = nil
|
n.salt = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (n *NoopAudit) RegisterNodesAndPipeline(broker *eventlogger.Broker, _ string) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
type TestLogger struct {
|
type TestLogger struct {
|
||||||
hclog.Logger
|
hclog.Logger
|
||||||
Path string
|
Path string
|
||||||
@@ -427,7 +444,7 @@ func NewTestLogger(t testing.T) *TestLogger {
|
|||||||
// We send nothing on the regular logger, that way we can later deregister
|
// We send nothing on the regular logger, that way we can later deregister
|
||||||
// the sink to stop logging during cluster cleanup.
|
// the sink to stop logging during cluster cleanup.
|
||||||
logger := hclog.NewInterceptLogger(&hclog.LoggerOptions{
|
logger := hclog.NewInterceptLogger(&hclog.LoggerOptions{
|
||||||
Output: ioutil.Discard,
|
Output: io.Discard,
|
||||||
IndependentLevels: true,
|
IndependentLevels: true,
|
||||||
})
|
})
|
||||||
sink := hclog.NewSinkAdapter(&hclog.LoggerOptions{
|
sink := hclog.NewSinkAdapter(&hclog.LoggerOptions{
|
||||||
|
|||||||
@@ -1,250 +0,0 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package event
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/hashicorp/eventlogger"
|
|
||||||
)
|
|
||||||
|
|
||||||
// defaultFileMode is the default file permissions (read/write for everyone).
|
|
||||||
const (
|
|
||||||
defaultFileMode = 0o600
|
|
||||||
discard = "discard"
|
|
||||||
stdout = "stdout"
|
|
||||||
)
|
|
||||||
|
|
||||||
// AuditFileSink is a sink node which handles writing audit events to file.
|
|
||||||
type AuditFileSink struct {
|
|
||||||
file *os.File
|
|
||||||
fileLock sync.RWMutex
|
|
||||||
fileMode os.FileMode
|
|
||||||
path string
|
|
||||||
format auditFormat
|
|
||||||
prefix string
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAuditFileSink should be used to create a new AuditFileSink.
|
|
||||||
// Accepted options: WithFileMode and WithPrefix.
|
|
||||||
func NewAuditFileSink(path string, format auditFormat, opt ...Option) (*AuditFileSink, error) {
|
|
||||||
const op = "event.NewAuditFileSink"
|
|
||||||
|
|
||||||
// Parse and check path
|
|
||||||
p := strings.TrimSpace(path)
|
|
||||||
switch {
|
|
||||||
case p == "":
|
|
||||||
return nil, fmt.Errorf("%s: path is required", op)
|
|
||||||
case strings.EqualFold(path, stdout):
|
|
||||||
p = stdout
|
|
||||||
case strings.EqualFold(path, discard):
|
|
||||||
p = discard
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate format
|
|
||||||
if err := format.validate(); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: invalid format: %w", op, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
opts, err := getOpts(opt...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: error applying options: %w", op, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mode := os.FileMode(defaultFileMode)
|
|
||||||
// If we got an optional file mode supplied and our path isn't a special keyword
|
|
||||||
// then we should use the supplied file mode, or maintain the existing file mode.
|
|
||||||
if opts.withFileMode != nil {
|
|
||||||
switch {
|
|
||||||
case p == stdout:
|
|
||||||
case p == discard:
|
|
||||||
case *opts.withFileMode == 0: // Maintain the existing file's mode when set to "0000".
|
|
||||||
fileInfo, err := os.Stat(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: unable to determine existing file mode: %w", op, err)
|
|
||||||
}
|
|
||||||
mode = fileInfo.Mode()
|
|
||||||
default:
|
|
||||||
mode = *opts.withFileMode
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return &AuditFileSink{
|
|
||||||
file: nil,
|
|
||||||
fileLock: sync.RWMutex{},
|
|
||||||
fileMode: mode,
|
|
||||||
format: format,
|
|
||||||
path: p,
|
|
||||||
prefix: opts.withPrefix,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process handles writing the event to the file sink.
|
|
||||||
func (f *AuditFileSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) {
|
|
||||||
const op = "event.(AuditFileSink).Process"
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
if e == nil {
|
|
||||||
return nil, fmt.Errorf("%s: event is nil: %w", op, ErrInvalidParameter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 'discard' path means we just do nothing and pretend we're done.
|
|
||||||
if f.path == discard {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
formatted, found := e.Format(f.format.String())
|
|
||||||
if !found {
|
|
||||||
return nil, fmt.Errorf("%s: unable to retrieve event formatted as %q", op, f.format)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := f.log(formatted)
|
|
||||||
if err != nil {
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reopen handles closing and reopening the file.
|
|
||||||
func (f *AuditFileSink) Reopen() error {
|
|
||||||
const op = "event.(AuditFileSink).Reopen"
|
|
||||||
|
|
||||||
switch f.path {
|
|
||||||
case stdout, discard:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
f.fileLock.Lock()
|
|
||||||
defer f.fileLock.Unlock()
|
|
||||||
|
|
||||||
if f.file == nil {
|
|
||||||
return f.open()
|
|
||||||
}
|
|
||||||
|
|
||||||
err := f.file.Close()
|
|
||||||
// Set to nil here so that even if we error out, on the next access open() will be tried.
|
|
||||||
f.file = nil
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: unable to close file for re-opening on audit sink: %w", op, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return f.open()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type describes the type of this node (sink).
|
|
||||||
func (f *AuditFileSink) Type() eventlogger.NodeType {
|
|
||||||
return eventlogger.NodeTypeSink
|
|
||||||
}
|
|
||||||
|
|
||||||
// open attempts to open a file at the sink's path, with the sink's fileMode permissions
|
|
||||||
// if one is not already open.
|
|
||||||
// It doesn't have any locking and relies on calling functions of AuditFileSink to
|
|
||||||
// handle this (e.g. log and Reopen methods).
|
|
||||||
func (f *AuditFileSink) open() error {
|
|
||||||
const op = "event.(AuditFileSink).open"
|
|
||||||
|
|
||||||
if f.file != nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(filepath.Dir(f.path), f.fileMode); err != nil {
|
|
||||||
return fmt.Errorf("%s: unable to create file %q: %w", op, f.path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
f.file, err = os.OpenFile(f.path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, f.fileMode)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: unable to open file for audit sink: %w", op, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Change the file mode in case the log file already existed.
|
|
||||||
// We special case '/dev/null' since we can't chmod it, and bypass if the mode is zero.
|
|
||||||
switch f.path {
|
|
||||||
case "/dev/null":
|
|
||||||
default:
|
|
||||||
if f.fileMode != 0 {
|
|
||||||
err = os.Chmod(f.path, f.fileMode)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: unable to change file %q permissions '%v' for audit sink: %w", op, f.path, f.fileMode, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// log writes the buffer to the file.
|
|
||||||
// It acquires a lock on the file to do this.
|
|
||||||
func (f *AuditFileSink) log(data []byte) error {
|
|
||||||
const op = "event.(AuditFileSink).log"
|
|
||||||
|
|
||||||
f.fileLock.Lock()
|
|
||||||
defer f.fileLock.Unlock()
|
|
||||||
|
|
||||||
reader := bytes.NewReader(data)
|
|
||||||
|
|
||||||
var writer io.Writer
|
|
||||||
switch {
|
|
||||||
case f.path == stdout:
|
|
||||||
writer = os.Stdout
|
|
||||||
default:
|
|
||||||
if err := f.open(); err != nil {
|
|
||||||
return fmt.Errorf("%s: unable to open file for audit sink: %w", op, err)
|
|
||||||
}
|
|
||||||
writer = f.file
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write prefix before the data if required.
|
|
||||||
if f.prefix != "" {
|
|
||||||
_, err := writer.Write([]byte(f.prefix))
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: unable to write prefix %q for audit sink: %w", op, f.prefix, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := reader.WriteTo(writer); err == nil {
|
|
||||||
return nil
|
|
||||||
} else if f.path == stdout {
|
|
||||||
// If writing to stdout there's no real reason to think anything would change on retry.
|
|
||||||
return fmt.Errorf("%s: unable write to %q: %w", op, f.path, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Otherwise, opportunistically try to re-open the FD, once per call (1 retry attempt).
|
|
||||||
err := f.file.Close()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: unable to close file for audit sink: %w", op, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
f.file = nil
|
|
||||||
|
|
||||||
if err := f.open(); err != nil {
|
|
||||||
return fmt.Errorf("%s: unable to re-open file for audit sink: %w", op, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = reader.Seek(0, io.SeekStart)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: unable to seek to start of file for audit sink: %w", op, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = reader.WriteTo(writer)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: unable to re-write to file for audit sink: %w", op, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,415 +0,0 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package event
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
|
||||||
|
|
||||||
vaultaudit "github.com/hashicorp/vault/audit"
|
|
||||||
|
|
||||||
"github.com/hashicorp/vault/helper/namespace"
|
|
||||||
|
|
||||||
"github.com/hashicorp/eventlogger"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestAuditFileSink_Type ensures that the node is a 'sink' type.
|
|
||||||
func TestAuditFileSink_Type(t *testing.T) {
|
|
||||||
f, err := NewAuditFileSink(t.TempDir(), AuditFormatJSON)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, f)
|
|
||||||
require.Equal(t, eventlogger.NodeTypeSink, f.Type())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestNewAuditFileSink tests creation of an AuditFileSink.
|
|
||||||
func TestNewAuditFileSink(t *testing.T) {
|
|
||||||
tests := map[string]struct {
|
|
||||||
IsTempDirPath bool // Path should contain the filename if temp dir is true
|
|
||||||
Path string
|
|
||||||
Format auditFormat
|
|
||||||
Options []Option
|
|
||||||
IsErrorExpected bool
|
|
||||||
ExpectedErrorMessage string
|
|
||||||
// Expected values of AuditFileSink
|
|
||||||
ExpectedFileMode os.FileMode
|
|
||||||
ExpectedFormat auditFormat
|
|
||||||
ExpectedPath string
|
|
||||||
ExpectedPrefix string
|
|
||||||
}{
|
|
||||||
"default-values": {
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "event.NewAuditFileSink: path is required",
|
|
||||||
},
|
|
||||||
"spacey-path": {
|
|
||||||
Path: " ",
|
|
||||||
Format: AuditFormatJSON,
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "event.NewAuditFileSink: path is required",
|
|
||||||
},
|
|
||||||
"bad-format": {
|
|
||||||
Path: "qwerty",
|
|
||||||
Format: "squirrels",
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "event.NewAuditFileSink: invalid format: event.(auditFormat).validate: 'squirrels' is not a valid format: invalid parameter",
|
|
||||||
},
|
|
||||||
"path-not-exist-valid-format-file-mode": {
|
|
||||||
Path: "qwerty",
|
|
||||||
Format: AuditFormatJSON,
|
|
||||||
Options: []Option{WithFileMode("00755")},
|
|
||||||
IsErrorExpected: false,
|
|
||||||
ExpectedPath: "qwerty",
|
|
||||||
ExpectedFormat: AuditFormatJSON,
|
|
||||||
ExpectedPrefix: "",
|
|
||||||
ExpectedFileMode: os.FileMode(0o755),
|
|
||||||
},
|
|
||||||
"valid-path-no-format": {
|
|
||||||
IsTempDirPath: true,
|
|
||||||
Path: "vault.log",
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "event.NewAuditFileSink: invalid format: event.(auditFormat).validate: '' is not a valid format: invalid parameter",
|
|
||||||
},
|
|
||||||
"valid-path-and-format": {
|
|
||||||
IsTempDirPath: true,
|
|
||||||
Path: "vault.log",
|
|
||||||
Format: AuditFormatJSON,
|
|
||||||
IsErrorExpected: false,
|
|
||||||
ExpectedFileMode: defaultFileMode,
|
|
||||||
ExpectedFormat: AuditFormatJSON,
|
|
||||||
ExpectedPrefix: "",
|
|
||||||
},
|
|
||||||
"file-mode-not-default-or-zero": {
|
|
||||||
Path: "vault.log",
|
|
||||||
Format: AuditFormatJSON,
|
|
||||||
Options: []Option{WithFileMode("0007")},
|
|
||||||
IsTempDirPath: true,
|
|
||||||
IsErrorExpected: false,
|
|
||||||
ExpectedFormat: AuditFormatJSON,
|
|
||||||
ExpectedPrefix: "",
|
|
||||||
ExpectedFileMode: 0o007,
|
|
||||||
},
|
|
||||||
"path-stdout": {
|
|
||||||
Path: "stdout",
|
|
||||||
Format: AuditFormatJSON,
|
|
||||||
Options: []Option{WithFileMode("0007")}, // Will be ignored as stdout
|
|
||||||
IsTempDirPath: false,
|
|
||||||
IsErrorExpected: false,
|
|
||||||
ExpectedPath: "stdout",
|
|
||||||
ExpectedFormat: AuditFormatJSON,
|
|
||||||
ExpectedPrefix: "",
|
|
||||||
ExpectedFileMode: defaultFileMode,
|
|
||||||
},
|
|
||||||
"path-discard": {
|
|
||||||
Path: "discard",
|
|
||||||
Format: AuditFormatJSON,
|
|
||||||
Options: []Option{WithFileMode("0007")},
|
|
||||||
IsTempDirPath: false,
|
|
||||||
IsErrorExpected: false,
|
|
||||||
ExpectedPath: "discard",
|
|
||||||
ExpectedFormat: AuditFormatJSON,
|
|
||||||
ExpectedPrefix: "",
|
|
||||||
ExpectedFileMode: defaultFileMode,
|
|
||||||
},
|
|
||||||
"prefix": {
|
|
||||||
IsTempDirPath: true,
|
|
||||||
Path: "vault.log",
|
|
||||||
Format: AuditFormatJSON,
|
|
||||||
Options: []Option{WithFileMode("0007"), WithPrefix("bleep")},
|
|
||||||
IsErrorExpected: false,
|
|
||||||
ExpectedPrefix: "bleep",
|
|
||||||
ExpectedFormat: AuditFormatJSON,
|
|
||||||
ExpectedFileMode: 0o007,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, tc := range tests {
|
|
||||||
name := name
|
|
||||||
tc := tc
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
// t.Parallel()
|
|
||||||
|
|
||||||
// If we need a real directory as a path we can use a temp dir.
|
|
||||||
// but we should keep track of it for comparison in the new sink.
|
|
||||||
var tempDir string
|
|
||||||
tempPath := tc.Path
|
|
||||||
if tc.IsTempDirPath {
|
|
||||||
tempDir = t.TempDir()
|
|
||||||
tempPath = filepath.Join(tempDir, tempPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
sink, err := NewAuditFileSink(tempPath, tc.Format, tc.Options...)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case tc.IsErrorExpected:
|
|
||||||
require.Error(t, err)
|
|
||||||
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
|
||||||
require.Nil(t, sink)
|
|
||||||
default:
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, sink)
|
|
||||||
|
|
||||||
// Assert properties are correct.
|
|
||||||
require.Equal(t, tc.ExpectedPrefix, sink.prefix)
|
|
||||||
require.Equal(t, tc.ExpectedFormat, sink.format)
|
|
||||||
require.Equal(t, tc.ExpectedFileMode, sink.fileMode)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case tc.IsTempDirPath:
|
|
||||||
require.Equal(t, tempPath, sink.path)
|
|
||||||
default:
|
|
||||||
require.Equal(t, tc.ExpectedPath, sink.path)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestAuditFileSink_Reopen tests that the sink reopens files as expected when requested to.
|
|
||||||
// stdout and discard paths are ignored.
|
|
||||||
// see: https://developer.hashicorp.com/vault/docs/audit/file#file_path
|
|
||||||
func TestAuditFileSink_Reopen(t *testing.T) {
|
|
||||||
tests := map[string]struct {
|
|
||||||
Path string
|
|
||||||
IsTempDirPath bool
|
|
||||||
ShouldCreateFile bool
|
|
||||||
Options []Option
|
|
||||||
IsErrorExpected bool
|
|
||||||
ExpectedErrorMessage string
|
|
||||||
ExpectedFileMode os.FileMode
|
|
||||||
}{
|
|
||||||
// Should be ignored by Reopen
|
|
||||||
"discard": {
|
|
||||||
Path: "discard",
|
|
||||||
},
|
|
||||||
// Should be ignored by Reopen
|
|
||||||
"stdout": {
|
|
||||||
Path: "stdout",
|
|
||||||
},
|
|
||||||
"permission-denied": {
|
|
||||||
Path: "/tmp/vault/test/foo.log",
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "event.(AuditFileSink).open: unable to create file \"/tmp/vault/test/foo.log\": mkdir /tmp/vault/test: permission denied",
|
|
||||||
},
|
|
||||||
"happy": {
|
|
||||||
Path: "vault.log",
|
|
||||||
IsTempDirPath: true,
|
|
||||||
ExpectedFileMode: os.FileMode(defaultFileMode),
|
|
||||||
},
|
|
||||||
"filemode-existing": {
|
|
||||||
Path: "vault.log",
|
|
||||||
IsTempDirPath: true,
|
|
||||||
ShouldCreateFile: true,
|
|
||||||
Options: []Option{WithFileMode("0000")},
|
|
||||||
ExpectedFileMode: os.FileMode(defaultFileMode),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, tc := range tests {
|
|
||||||
name := name
|
|
||||||
tc := tc
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
// If we need a real directory as a path we can use a temp dir.
|
|
||||||
// but we should keep track of it for comparison in the new sink.
|
|
||||||
var tempDir string
|
|
||||||
tempPath := tc.Path
|
|
||||||
if tc.IsTempDirPath {
|
|
||||||
tempDir = t.TempDir()
|
|
||||||
tempPath = filepath.Join(tempDir, tc.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the file mode is 0 then we will need a pre-created file to stat.
|
|
||||||
// Only do this for paths that are not 'special keywords'
|
|
||||||
if tc.ShouldCreateFile && tc.Path != discard && tc.Path != stdout {
|
|
||||||
f, err := os.OpenFile(tempPath, os.O_CREATE, defaultFileMode)
|
|
||||||
require.NoError(t, err)
|
|
||||||
defer func() {
|
|
||||||
err = os.Remove(f.Name())
|
|
||||||
require.NoError(t, err)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
sink, err := NewAuditFileSink(tempPath, AuditFormatJSON, tc.Options...)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, sink)
|
|
||||||
|
|
||||||
err = sink.Reopen()
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case tc.IsErrorExpected:
|
|
||||||
require.Error(t, err)
|
|
||||||
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
|
||||||
case tempPath == discard:
|
|
||||||
require.NoError(t, err)
|
|
||||||
case tempPath == stdout:
|
|
||||||
require.NoError(t, err)
|
|
||||||
default:
|
|
||||||
require.NoError(t, err)
|
|
||||||
info, err := os.Stat(tempPath)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, info)
|
|
||||||
require.Equal(t, tc.ExpectedFileMode, info.Mode())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestAuditFileSink_Process ensures that Process behaves as expected.
|
|
||||||
func TestAuditFileSink_Process(t *testing.T) {
|
|
||||||
tests := map[string]struct {
|
|
||||||
Path string
|
|
||||||
Format auditFormat
|
|
||||||
Data *logical.LogInput
|
|
||||||
ShouldIgnoreFormat bool
|
|
||||||
ShouldUseNilEvent bool
|
|
||||||
IsErrorExpected bool
|
|
||||||
ExpectedErrorMessage string
|
|
||||||
}{
|
|
||||||
"discard": {
|
|
||||||
Path: discard,
|
|
||||||
Format: AuditFormatJSON,
|
|
||||||
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
|
||||||
IsErrorExpected: false,
|
|
||||||
},
|
|
||||||
"stdout": {
|
|
||||||
Path: stdout,
|
|
||||||
Format: AuditFormatJSON,
|
|
||||||
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
|
||||||
IsErrorExpected: false,
|
|
||||||
},
|
|
||||||
"no-formatted-data": {
|
|
||||||
Path: "/dev/null",
|
|
||||||
Format: AuditFormatJSON,
|
|
||||||
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
|
||||||
ShouldIgnoreFormat: true,
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "event.(AuditFileSink).Process: unable to retrieve event formatted as \"json\"",
|
|
||||||
},
|
|
||||||
"nil": {
|
|
||||||
Path: "/dev/null",
|
|
||||||
Format: AuditFormatJSON,
|
|
||||||
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
|
||||||
ShouldUseNilEvent: true,
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "event.(AuditFileSink).Process: event is nil: invalid parameter",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, tc := range tests {
|
|
||||||
name := name
|
|
||||||
tc := tc
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
// Setup a formatter
|
|
||||||
cfg := vaultaudit.FormatterConfig{}
|
|
||||||
ss := newStaticSalt(t)
|
|
||||||
formatter, err := NewAuditFormatterJSON(cfg, ss)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, formatter)
|
|
||||||
|
|
||||||
// Setup a sink
|
|
||||||
sink, err := NewAuditFileSink(tc.Path, tc.Format)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, sink)
|
|
||||||
|
|
||||||
// Generate a fake event
|
|
||||||
ctx := namespace.RootContext(nil)
|
|
||||||
event := fakeJSONAuditEvent(t, AuditRequest, tc.Data)
|
|
||||||
require.NotNil(t, event)
|
|
||||||
|
|
||||||
// Finesse the event into the correct shape.
|
|
||||||
event, err = formatter.Process(ctx, event)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, event)
|
|
||||||
|
|
||||||
// Some conditional stuff 'per test' to exercise different parts of Process.
|
|
||||||
if tc.ShouldIgnoreFormat {
|
|
||||||
delete(event.Formatted, tc.Format.String())
|
|
||||||
}
|
|
||||||
|
|
||||||
if tc.ShouldUseNilEvent {
|
|
||||||
event = nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// The actual exercising of the sink.
|
|
||||||
event, err = sink.Process(ctx, event)
|
|
||||||
switch {
|
|
||||||
case tc.IsErrorExpected:
|
|
||||||
require.Error(t, err)
|
|
||||||
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
|
||||||
require.Nil(t, event)
|
|
||||||
default:
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Nil(t, event)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// BenchmarkAuditFileSink_Process benchmarks the AuditFormatterJSON and then AuditFileSink calling Process.
|
|
||||||
// This should replicate the original benchmark testing which used to perform both of these roles together.
|
|
||||||
func BenchmarkAuditFileSink_Process(b *testing.B) {
|
|
||||||
// Base input
|
|
||||||
in := &logical.LogInput{
|
|
||||||
Auth: &logical.Auth{
|
|
||||||
ClientToken: "foo",
|
|
||||||
Accessor: "bar",
|
|
||||||
EntityID: "foobarentity",
|
|
||||||
DisplayName: "testtoken",
|
|
||||||
NoDefaultPolicy: true,
|
|
||||||
Policies: []string{"root"},
|
|
||||||
TokenType: logical.TokenTypeService,
|
|
||||||
},
|
|
||||||
Request: &logical.Request{
|
|
||||||
Operation: logical.UpdateOperation,
|
|
||||||
Path: "/foo",
|
|
||||||
Connection: &logical.Connection{
|
|
||||||
RemoteAddr: "127.0.0.1",
|
|
||||||
},
|
|
||||||
WrapInfo: &logical.RequestWrapInfo{
|
|
||||||
TTL: 60 * time.Second,
|
|
||||||
},
|
|
||||||
Headers: map[string][]string{
|
|
||||||
"foo": {"bar"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := namespace.RootContext(nil)
|
|
||||||
|
|
||||||
// Create the formatter node.
|
|
||||||
cfg := vaultaudit.FormatterConfig{}
|
|
||||||
ss := newStaticSalt(b)
|
|
||||||
formatter, err := NewAuditFormatterJSON(cfg, ss)
|
|
||||||
require.NoError(b, err)
|
|
||||||
require.NotNil(b, formatter)
|
|
||||||
|
|
||||||
// Create the sink node.
|
|
||||||
sink, err := NewAuditFileSink("/dev/null", AuditFormatJSON)
|
|
||||||
require.NoError(b, err)
|
|
||||||
require.NotNil(b, sink)
|
|
||||||
|
|
||||||
// Generate the event
|
|
||||||
event := fakeJSONAuditEvent(b, AuditRequest, in)
|
|
||||||
require.NotNil(b, event)
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
b.RunParallel(func(pb *testing.PB) {
|
|
||||||
for pb.Next() {
|
|
||||||
event, err = formatter.Process(ctx, event)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
_, err := sink.Process(ctx, event)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -1,135 +0,0 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package event
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Audit subtypes.
|
|
||||||
const (
|
|
||||||
AuditRequest auditSubtype = "AuditRequest"
|
|
||||||
AuditResponse auditSubtype = "AuditResponse"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Audit formats.
|
|
||||||
const (
|
|
||||||
AuditFormatJSON auditFormat = "json"
|
|
||||||
AuditFormatJSONx auditFormat = "jsonx"
|
|
||||||
)
|
|
||||||
|
|
||||||
// auditVersion defines the version of audit events.
|
|
||||||
const auditVersion = "v0.1"
|
|
||||||
|
|
||||||
// auditSubtype defines the type of audit event.
|
|
||||||
type auditSubtype string
|
|
||||||
|
|
||||||
// auditFormat defines types of format audit events support.
|
|
||||||
type auditFormat string
|
|
||||||
|
|
||||||
// audit is the audit event.
|
|
||||||
type audit struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Version string `json:"version"`
|
|
||||||
Subtype auditSubtype `json:"subtype"` // the subtype of the audit event.
|
|
||||||
Timestamp time.Time `json:"timestamp"`
|
|
||||||
Data *logical.LogInput `json:"data"`
|
|
||||||
RequiredFormat auditFormat `json:"format"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// newAudit should be used to create an audit event.
|
|
||||||
// auditSubtype and auditFormat are needed for audit.
|
|
||||||
// It will use the supplied options, generate an ID if required, and validate the event.
|
|
||||||
func newAudit(opt ...Option) (*audit, error) {
|
|
||||||
const op = "event.newAudit"
|
|
||||||
|
|
||||||
opts, err := getOpts(opt...)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: error applying options: %w", op, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if opts.withID == "" {
|
|
||||||
var err error
|
|
||||||
|
|
||||||
opts.withID, err = NewID(string(AuditType))
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: error creating ID for event: %w", op, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
audit := &audit{
|
|
||||||
ID: opts.withID,
|
|
||||||
Version: auditVersion,
|
|
||||||
Subtype: opts.withSubtype,
|
|
||||||
Timestamp: opts.withNow,
|
|
||||||
RequiredFormat: opts.withFormat,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := audit.validate(); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: %w", op, err)
|
|
||||||
}
|
|
||||||
return audit, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate attempts to ensure the event has the basic requirements of the event type configured.
|
|
||||||
func (a *audit) validate() error {
|
|
||||||
const op = "event.(audit).validate"
|
|
||||||
if a == nil {
|
|
||||||
return fmt.Errorf("%s: audit is nil: %w", op, ErrInvalidParameter)
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.ID == "" {
|
|
||||||
return fmt.Errorf("%s: missing ID: %w", op, ErrInvalidParameter)
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.Version != auditVersion {
|
|
||||||
return fmt.Errorf("%s: audit version unsupported: %w", op, ErrInvalidParameter)
|
|
||||||
}
|
|
||||||
|
|
||||||
if a.Timestamp.IsZero() {
|
|
||||||
return fmt.Errorf("%s: audit timestamp cannot be the zero time instant: %w", op, ErrInvalidParameter)
|
|
||||||
}
|
|
||||||
|
|
||||||
err := a.Subtype.validate()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", op, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = a.RequiredFormat.validate()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", op, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate ensures that auditSubtype is one of the set of allowed event subtypes.
|
|
||||||
func (t auditSubtype) validate() error {
|
|
||||||
const op = "event.(auditSubtype).validate"
|
|
||||||
switch t {
|
|
||||||
case AuditRequest, AuditResponse:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%s: '%s' is not a valid event subtype: %w", op, t, ErrInvalidParameter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// validate ensures that auditFormat is one of the set of allowed event formats.
|
|
||||||
func (f auditFormat) validate() error {
|
|
||||||
const op = "event.(auditFormat).validate"
|
|
||||||
switch f {
|
|
||||||
case AuditFormatJSON, AuditFormatJSONx:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%s: '%s' is not a valid format: %w", op, f, ErrInvalidParameter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// String returns the string version of an auditFormat.
|
|
||||||
func (f auditFormat) String() string {
|
|
||||||
return string(f)
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package event
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
vaultaudit "github.com/hashicorp/vault/audit"
|
|
||||||
"github.com/hashicorp/vault/sdk/helper/jsonutil"
|
|
||||||
|
|
||||||
"github.com/hashicorp/eventlogger"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ eventlogger.Node = (*AuditFormatterJSON)(nil)
|
|
||||||
|
|
||||||
// AuditFormatterJSON represents the formatter node which is used to handle
|
|
||||||
// formatting audit events as JSON.
|
|
||||||
type AuditFormatterJSON struct {
|
|
||||||
config vaultaudit.FormatterConfig
|
|
||||||
format auditFormat
|
|
||||||
formatter vaultaudit.Formatter
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAuditFormatterJSON should be used to create an AuditFormatterJSON.
|
|
||||||
func NewAuditFormatterJSON(config vaultaudit.FormatterConfig, salter vaultaudit.Salter) (*AuditFormatterJSON, error) {
|
|
||||||
const op = "event.NewAuditFormatterJSON"
|
|
||||||
|
|
||||||
f, err := vaultaudit.NewAuditFormatter(salter)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: unable to create new JSON audit formatter: %w", op, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonFormatter := &AuditFormatterJSON{
|
|
||||||
format: AuditFormatJSON,
|
|
||||||
config: config,
|
|
||||||
formatter: f,
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonFormatter, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reopen is a no-op for a formatter node.
|
|
||||||
func (_ *AuditFormatterJSON) Reopen() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type describes the type of this node (formatter).
|
|
||||||
func (_ *AuditFormatterJSON) Type() eventlogger.NodeType {
|
|
||||||
return eventlogger.NodeTypeFormatter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process will attempt to parse the incoming event data into a corresponding
|
|
||||||
// audit request/response entry which is serialized to JSON and stored within the event.
|
|
||||||
func (f *AuditFormatterJSON) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) {
|
|
||||||
const op = "event.(AuditFormatterJSON).Process"
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
if e == nil {
|
|
||||||
return nil, fmt.Errorf("%s: event is nil: %w", op, ErrInvalidParameter)
|
|
||||||
}
|
|
||||||
|
|
||||||
a, ok := e.Payload.(*audit)
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("%s: cannot parse event payload: %w", op, ErrInvalidParameter)
|
|
||||||
}
|
|
||||||
|
|
||||||
var formatted []byte
|
|
||||||
|
|
||||||
switch a.Subtype {
|
|
||||||
case AuditRequest:
|
|
||||||
entry, err := f.formatter.FormatRequest(ctx, f.config, a.Data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: unable to parse request from audit event: %w", op, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
formatted, err = jsonutil.EncodeJSON(entry)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: unable to format request: %w", op, err)
|
|
||||||
}
|
|
||||||
case AuditResponse:
|
|
||||||
entry, err := f.formatter.FormatResponse(ctx, f.config, a.Data)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: unable to parse response from audit event: %w", op, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
formatted, err = jsonutil.EncodeJSON(entry)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: unable to format response: %w", op, err)
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("%s: unknown audit event subtype: %q", op, a.Subtype)
|
|
||||||
}
|
|
||||||
|
|
||||||
e.FormattedAs(f.format.String(), formatted)
|
|
||||||
|
|
||||||
return e, nil
|
|
||||||
}
|
|
||||||
@@ -1,242 +0,0 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package event
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hashicorp/vault/helper/namespace"
|
|
||||||
|
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
|
||||||
|
|
||||||
vaultaudit "github.com/hashicorp/vault/audit"
|
|
||||||
|
|
||||||
"github.com/hashicorp/vault/sdk/helper/salt"
|
|
||||||
|
|
||||||
"github.com/hashicorp/eventlogger"
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// fakeJSONAuditEvent will return a new fake event containing audit data based
|
|
||||||
// on the specified auditSubtype and logical.LogInput.
|
|
||||||
func fakeJSONAuditEvent(tb testing.TB, subtype auditSubtype, input *logical.LogInput) *eventlogger.Event {
|
|
||||||
tb.Helper()
|
|
||||||
|
|
||||||
date := time.Date(2023, time.July, 11, 15, 49, 10, 0o0, time.Local)
|
|
||||||
|
|
||||||
auditEvent, err := newAudit(
|
|
||||||
WithID("123"),
|
|
||||||
WithSubtype(string(subtype)),
|
|
||||||
WithFormat(string(AuditFormatJSON)),
|
|
||||||
WithNow(date),
|
|
||||||
)
|
|
||||||
require.NoError(tb, err)
|
|
||||||
require.NotNil(tb, auditEvent)
|
|
||||||
require.Equal(tb, "123", auditEvent.ID)
|
|
||||||
require.Equal(tb, "v0.1", auditEvent.Version)
|
|
||||||
require.Equal(tb, AuditFormatJSON, auditEvent.RequiredFormat)
|
|
||||||
require.Equal(tb, subtype, auditEvent.Subtype)
|
|
||||||
require.Equal(tb, date, auditEvent.Timestamp)
|
|
||||||
|
|
||||||
auditEvent.Data = input
|
|
||||||
|
|
||||||
e := &eventlogger.Event{
|
|
||||||
Type: eventlogger.EventType(AuditType),
|
|
||||||
CreatedAt: auditEvent.Timestamp,
|
|
||||||
Formatted: make(map[string][]byte),
|
|
||||||
Payload: auditEvent,
|
|
||||||
}
|
|
||||||
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
// newStaticSalt returns a new staticSalt for use in testing.
|
|
||||||
func newStaticSalt(tb testing.TB) *staticSalt {
|
|
||||||
s, err := salt.NewSalt(context.Background(), nil, nil)
|
|
||||||
require.NoError(tb, err)
|
|
||||||
|
|
||||||
return &staticSalt{salt: s}
|
|
||||||
}
|
|
||||||
|
|
||||||
// staticSalt is a struct which can be used to obtain a static salt.
|
|
||||||
// a salt must be assigned when the struct is initialized.
|
|
||||||
type staticSalt struct {
|
|
||||||
salt *salt.Salt
|
|
||||||
}
|
|
||||||
|
|
||||||
// Salt returns the static salt and no error.
|
|
||||||
func (s *staticSalt) Salt(_ context.Context) (*salt.Salt, error) {
|
|
||||||
return s.salt, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestNewAuditFormatterJSON ensures we can create new AuditFormatterJSONX structs.
|
|
||||||
func TestNewAuditFormatterJSON(t *testing.T) {
|
|
||||||
tests := map[string]struct {
|
|
||||||
UseStaticSalt bool
|
|
||||||
IsErrorExpected bool
|
|
||||||
ExpectedErrorMessage string
|
|
||||||
}{
|
|
||||||
"nil-salter": {
|
|
||||||
UseStaticSalt: false,
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "event.NewAuditFormatterJSON: unable to create new JSON audit formatter: cannot create a new audit formatter with nil salter",
|
|
||||||
},
|
|
||||||
"static-salter": {
|
|
||||||
UseStaticSalt: true,
|
|
||||||
IsErrorExpected: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, tc := range tests {
|
|
||||||
name := name
|
|
||||||
tc := tc
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
cfg := vaultaudit.FormatterConfig{}
|
|
||||||
var ss vaultaudit.Salter
|
|
||||||
if tc.UseStaticSalt {
|
|
||||||
ss = newStaticSalt(t)
|
|
||||||
}
|
|
||||||
|
|
||||||
f, err := NewAuditFormatterJSON(cfg, ss)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case tc.IsErrorExpected:
|
|
||||||
require.Error(t, err)
|
|
||||||
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
|
||||||
require.Nil(t, f)
|
|
||||||
default:
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, f)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestAuditFormatterJSONX_Reopen ensures that we do no get an error when calling Reopen.
|
|
||||||
func TestAuditFormatterJSON_Reopen(t *testing.T) {
|
|
||||||
ss := newStaticSalt(t)
|
|
||||||
cfg := vaultaudit.FormatterConfig{}
|
|
||||||
|
|
||||||
f, err := NewAuditFormatterJSON(cfg, ss)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, f)
|
|
||||||
require.NoError(t, f.Reopen())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestAuditFormatterJSONX_Type ensures that the node is a 'formatter' type.
|
|
||||||
func TestAuditFormatterJSON_Type(t *testing.T) {
|
|
||||||
ss := newStaticSalt(t)
|
|
||||||
cfg := vaultaudit.FormatterConfig{}
|
|
||||||
|
|
||||||
f, err := NewAuditFormatterJSON(cfg, ss)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, f)
|
|
||||||
require.Equal(t, eventlogger.NodeTypeFormatter, f.Type())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestAuditFormatterJSON_Process attempts to run the Process method to convert
|
|
||||||
// the logical.LogInput within an audit event to JSON (AuditRequestEntry or AuditResponseEntry).
|
|
||||||
func TestAuditFormatterJSON_Process(t *testing.T) {
|
|
||||||
tests := map[string]struct {
|
|
||||||
IsErrorExpected bool
|
|
||||||
ExpectedErrorMessage string
|
|
||||||
Subtype auditSubtype
|
|
||||||
Data *logical.LogInput
|
|
||||||
RootNamespace bool
|
|
||||||
}{
|
|
||||||
"request-no-data": {
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "event.(AuditFormatterJSON).Process: unable to parse request from audit event: request to request-audit a nil request",
|
|
||||||
Subtype: AuditRequest,
|
|
||||||
Data: nil,
|
|
||||||
},
|
|
||||||
"response-no-data": {
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "event.(AuditFormatterJSON).Process: unable to parse response from audit event: request to response-audit a nil request",
|
|
||||||
Subtype: AuditResponse,
|
|
||||||
Data: nil,
|
|
||||||
},
|
|
||||||
"request-basic-input": {
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "event.(AuditFormatterJSON).Process: unable to parse request from audit event: request to request-audit a nil request",
|
|
||||||
Subtype: AuditRequest,
|
|
||||||
Data: &logical.LogInput{Type: "magic"},
|
|
||||||
},
|
|
||||||
"response-basic-input": {
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "event.(AuditFormatterJSON).Process: unable to parse response from audit event: request to response-audit a nil request",
|
|
||||||
Subtype: AuditResponse,
|
|
||||||
Data: &logical.LogInput{Type: "magic"},
|
|
||||||
},
|
|
||||||
"request-basic-input-and-request-no-ns": {
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "event.(AuditFormatterJSON).Process: unable to parse request from audit event: no namespace",
|
|
||||||
Subtype: AuditRequest,
|
|
||||||
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
|
||||||
},
|
|
||||||
"response-basic-input-and-request-no-ns": {
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "event.(AuditFormatterJSON).Process: unable to parse response from audit event: no namespace",
|
|
||||||
Subtype: AuditResponse,
|
|
||||||
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
|
||||||
},
|
|
||||||
"request-basic-input-and-request-with-ns": {
|
|
||||||
IsErrorExpected: false,
|
|
||||||
Subtype: AuditRequest,
|
|
||||||
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
|
||||||
RootNamespace: true,
|
|
||||||
},
|
|
||||||
"response-basic-input-and-request-with-ns": {
|
|
||||||
IsErrorExpected: false,
|
|
||||||
Subtype: AuditResponse,
|
|
||||||
Data: &logical.LogInput{Request: &logical.Request{ID: "123"}},
|
|
||||||
RootNamespace: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, tc := range tests {
|
|
||||||
name := name
|
|
||||||
tc := tc
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
e := fakeJSONAuditEvent(t, tc.Subtype, tc.Data)
|
|
||||||
require.NotNil(t, e)
|
|
||||||
|
|
||||||
ss := newStaticSalt(t)
|
|
||||||
cfg := vaultaudit.FormatterConfig{}
|
|
||||||
|
|
||||||
f, err := NewAuditFormatterJSON(cfg, ss)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, f)
|
|
||||||
|
|
||||||
var ctx context.Context
|
|
||||||
switch {
|
|
||||||
case tc.RootNamespace:
|
|
||||||
ctx = namespace.RootContext(context.Background())
|
|
||||||
default:
|
|
||||||
ctx = context.Background()
|
|
||||||
}
|
|
||||||
|
|
||||||
processed, err := f.Process(ctx, e)
|
|
||||||
b, found := e.Format(string(AuditFormatJSON))
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case tc.IsErrorExpected:
|
|
||||||
require.Error(t, err)
|
|
||||||
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
|
||||||
require.Nil(t, processed)
|
|
||||||
require.False(t, found)
|
|
||||||
require.Nil(t, b)
|
|
||||||
default:
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, processed)
|
|
||||||
require.True(t, found)
|
|
||||||
require.NotNil(t, b)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package event
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/hashicorp/eventlogger"
|
|
||||||
"github.com/jefferai/jsonx"
|
|
||||||
)
|
|
||||||
|
|
||||||
var _ eventlogger.Node = (*AuditFormatterJSONx)(nil)
|
|
||||||
|
|
||||||
// AuditFormatterJSONx represents a formatter node which will Process JSON to JSONx format.
|
|
||||||
type AuditFormatterJSONx struct {
|
|
||||||
format auditFormat
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewAuditFormatterJSONx creates a formatter node which can be used to format
|
|
||||||
// incoming events to JSONx.
|
|
||||||
// This formatter node requires that a AuditFormatterJSON node exists earlier
|
|
||||||
// in the pipeline and will attempt to access the JSON encoded data stored by that
|
|
||||||
// formatter node.
|
|
||||||
func NewAuditFormatterJSONx() *AuditFormatterJSONx {
|
|
||||||
return &AuditFormatterJSONx{format: AuditFormatJSONx}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reopen is a no-op for this formatter node.
|
|
||||||
func (_ *AuditFormatterJSONx) Reopen() error {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Type describes the type of this node.
|
|
||||||
func (_ *AuditFormatterJSONx) Type() eventlogger.NodeType {
|
|
||||||
return eventlogger.NodeTypeFormatter
|
|
||||||
}
|
|
||||||
|
|
||||||
// Process will attempt to retrieve pre-formatted JSON stored within the event
|
|
||||||
// and re-encode the data to JSONx.
|
|
||||||
func (f *AuditFormatterJSONx) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) {
|
|
||||||
const op = "event.(AuditFormatterJSONx).Process"
|
|
||||||
|
|
||||||
select {
|
|
||||||
case <-ctx.Done():
|
|
||||||
return nil, ctx.Err()
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
|
|
||||||
if e == nil {
|
|
||||||
return nil, fmt.Errorf("%s: event is nil: %w", op, ErrInvalidParameter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We expect that JSON has already been parsed for this event.
|
|
||||||
jsonBytes, ok := e.Format(AuditFormatJSON.String())
|
|
||||||
if !ok {
|
|
||||||
return nil, fmt.Errorf("%s: pre-formatted JSON required but not found: %w", op, ErrInvalidParameter)
|
|
||||||
}
|
|
||||||
if jsonBytes == nil {
|
|
||||||
return nil, fmt.Errorf("%s: pre-formatted JSON required but was nil: %w", op, ErrInvalidParameter)
|
|
||||||
}
|
|
||||||
|
|
||||||
xmlBytes, err := jsonx.EncodeJSONBytes(jsonBytes)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: unable to encode JSONx using JSON data: %w", op, err)
|
|
||||||
}
|
|
||||||
if xmlBytes == nil {
|
|
||||||
return nil, fmt.Errorf("%s: encoded JSONx was nil: %w", op, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
e.FormattedAs(f.format.String(), xmlBytes)
|
|
||||||
|
|
||||||
return e, nil
|
|
||||||
}
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package event
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/hashicorp/vault/sdk/logical"
|
|
||||||
|
|
||||||
"github.com/hashicorp/vault/sdk/helper/jsonutil"
|
|
||||||
|
|
||||||
"github.com/hashicorp/eventlogger"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// fakeJSONxAuditEvent will return a new fake event containing audit data based
|
|
||||||
// on the specified auditSubtype and logical.LogInput.
|
|
||||||
func fakeJSONxAuditEvent(t *testing.T, subtype auditSubtype, input *logical.LogInput) *eventlogger.Event {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
date := time.Date(2023, time.July, 11, 15, 49, 10, 0, time.Local)
|
|
||||||
|
|
||||||
auditEvent, err := newAudit(
|
|
||||||
WithID("123"),
|
|
||||||
WithSubtype(string(subtype)),
|
|
||||||
WithFormat(string(AuditFormatJSONx)),
|
|
||||||
WithNow(date),
|
|
||||||
)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, auditEvent)
|
|
||||||
require.Equal(t, "123", auditEvent.ID)
|
|
||||||
require.Equal(t, "v0.1", auditEvent.Version)
|
|
||||||
require.Equal(t, AuditFormatJSONx, auditEvent.RequiredFormat)
|
|
||||||
require.Equal(t, subtype, auditEvent.Subtype)
|
|
||||||
require.Equal(t, date, auditEvent.Timestamp)
|
|
||||||
|
|
||||||
auditEvent.Data = input
|
|
||||||
|
|
||||||
e := &eventlogger.Event{
|
|
||||||
Type: eventlogger.EventType(AuditType),
|
|
||||||
CreatedAt: auditEvent.Timestamp,
|
|
||||||
Formatted: make(map[string][]byte),
|
|
||||||
Payload: auditEvent,
|
|
||||||
}
|
|
||||||
|
|
||||||
return e
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestNewAuditFormatterJSONx ensures we can create new AuditFormatterJSONx structs.
|
|
||||||
func TestNewAuditFormatterJSONx(t *testing.T) {
|
|
||||||
f := NewAuditFormatterJSONx()
|
|
||||||
require.NotNil(t, f)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestAuditFormatterJSONx_Reopen ensures that we do no get an error when calling Reopen.
|
|
||||||
func TestAuditFormatterJSONx_Reopen(t *testing.T) {
|
|
||||||
require.NoError(t, NewAuditFormatterJSONx().Reopen())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestAuditFormatterJSONx_Type ensures that the node is a 'formatter' type.
|
|
||||||
func TestAuditFormatterJSONx_Type(t *testing.T) {
|
|
||||||
require.Equal(t, eventlogger.NodeTypeFormatter, NewAuditFormatterJSONx().Type())
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestAuditFormatterJSONx_Process attempts to run the Process method to convert
|
|
||||||
// pre-formatted JSON to XML (JSONx).
|
|
||||||
func TestAuditFormatterJSONx_Process(t *testing.T) {
|
|
||||||
tests := map[string]struct {
|
|
||||||
IsErrorExpected bool
|
|
||||||
ExpectedErrorMessage string
|
|
||||||
Subtype auditSubtype
|
|
||||||
Data *logical.LogInput
|
|
||||||
}{
|
|
||||||
"request-no-formatted-json": {
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "event.(AuditFormatterJSONx).Process: pre-formatted JSON required but not found: invalid parameter",
|
|
||||||
Subtype: AuditRequest,
|
|
||||||
Data: nil,
|
|
||||||
},
|
|
||||||
"response-no-formatted-json": {
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "event.(AuditFormatterJSONx).Process: pre-formatted JSON required but not found: invalid parameter",
|
|
||||||
Subtype: AuditResponse,
|
|
||||||
Data: nil,
|
|
||||||
},
|
|
||||||
"request-basic-json": {
|
|
||||||
IsErrorExpected: false,
|
|
||||||
Subtype: AuditRequest,
|
|
||||||
Data: &logical.LogInput{Type: "magic"},
|
|
||||||
},
|
|
||||||
"response-basic-json": {
|
|
||||||
IsErrorExpected: false,
|
|
||||||
Subtype: AuditResponse,
|
|
||||||
Data: &logical.LogInput{Type: "magic"},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, tc := range tests {
|
|
||||||
name := name
|
|
||||||
tc := tc
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
e := fakeJSONxAuditEvent(t, tc.Subtype, tc.Data)
|
|
||||||
require.NotNil(t, e)
|
|
||||||
|
|
||||||
// If we have data specified, then encode it and store as a format.
|
|
||||||
// This is faking the behavior of the JSON formatter node which is a
|
|
||||||
// pre-req for JSONx formatter node.
|
|
||||||
if tc.Data != nil {
|
|
||||||
jsonBytes, err := jsonutil.EncodeJSON(tc.Data)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, jsonBytes)
|
|
||||||
e.FormattedAs(string(AuditFormatJSON), jsonBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
processed, err := NewAuditFormatterJSONx().Process(context.Background(), e)
|
|
||||||
b, found := e.Format(string(AuditFormatJSONx))
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case tc.IsErrorExpected:
|
|
||||||
require.Error(t, err)
|
|
||||||
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
|
||||||
require.Nil(t, processed)
|
|
||||||
require.False(t, found)
|
|
||||||
require.Nil(t, b)
|
|
||||||
default:
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.NotNil(t, processed)
|
|
||||||
require.True(t, found)
|
|
||||||
require.NotNil(t, b)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -19,21 +19,18 @@ import (
|
|||||||
// Option is how Options are passed as arguments.
|
// Option is how Options are passed as arguments.
|
||||||
type Option func(*options) error
|
type Option func(*options) error
|
||||||
|
|
||||||
// options are used to represent configuration for an Event.
|
// Options are used to represent configuration for an Event.
|
||||||
type options struct {
|
type options struct {
|
||||||
withID string
|
withID string
|
||||||
withNow time.Time
|
withNow time.Time
|
||||||
withSubtype auditSubtype
|
|
||||||
withFormat auditFormat
|
|
||||||
withFileMode *os.FileMode
|
|
||||||
withPrefix string
|
|
||||||
withFacility string
|
withFacility string
|
||||||
withTag string
|
withTag string
|
||||||
withSocketType string
|
withSocketType string
|
||||||
withMaxDuration time.Duration
|
withMaxDuration time.Duration
|
||||||
|
withFileMode *os.FileMode
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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(),
|
||||||
@@ -44,7 +41,7 @@ func getDefaultOptions() options {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// getOpts applies all the supplied Option and returns configured options.
|
// getOpts applies all the supplied Option and returns configured Options.
|
||||||
// Each Option is applied in the order it appears in the argument list, so it is
|
// Each Option is applied in the order it appears in the argument list, so it is
|
||||||
// possible to supply the same Option numerous times and the 'last write wins'.
|
// possible to supply the same Option numerous times and the 'last write wins'.
|
||||||
func getOpts(opt ...Option) (options, error) {
|
func getOpts(opt ...Option) (options, error) {
|
||||||
@@ -110,51 +107,72 @@ func WithNow(now time.Time) Option {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithSubtype provides an option to represent the subtype.
|
// WithFacility provides an Option to represent a 'facility' for a syslog sink.
|
||||||
func WithSubtype(subtype string) Option {
|
func WithFacility(facility string) Option {
|
||||||
return func(o *options) error {
|
return func(o *options) error {
|
||||||
s := strings.TrimSpace(subtype)
|
facility = strings.TrimSpace(facility)
|
||||||
if s == "" {
|
|
||||||
return errors.New("subtype cannot be empty")
|
if facility != "" {
|
||||||
|
o.withFacility = facility
|
||||||
}
|
}
|
||||||
|
|
||||||
parsed := auditSubtype(s)
|
|
||||||
err := parsed.validate()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
o.withSubtype = parsed
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithFormat provides an option to represent event format.
|
// WithTag provides an Option to represent a 'tag' for a syslog sink.
|
||||||
func WithFormat(format string) Option {
|
func WithTag(tag string) Option {
|
||||||
return func(o *options) error {
|
return func(o *options) error {
|
||||||
f := strings.TrimSpace(format)
|
tag = strings.TrimSpace(tag)
|
||||||
if f == "" {
|
|
||||||
return errors.New("format cannot be empty")
|
if tag != "" {
|
||||||
|
o.withTag = tag
|
||||||
}
|
}
|
||||||
|
|
||||||
parsed := auditFormat(f)
|
|
||||||
err := parsed.validate()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
o.withFormat = parsed
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithFileMode provides an option to represent a file mode for a file sink.
|
// WithSocketType provides an Option to represent the socket type for a socket sink.
|
||||||
// Supplying an empty string or whitespace will prevent this option from being
|
func WithSocketType(socketType string) Option {
|
||||||
|
return func(o *options) error {
|
||||||
|
socketType = strings.TrimSpace(socketType)
|
||||||
|
|
||||||
|
if socketType != "" {
|
||||||
|
o.withSocketType = socketType
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithMaxDuration provides an Option to represent the max duration for writing to a socket.
|
||||||
|
func WithMaxDuration(duration string) Option {
|
||||||
|
return func(o *options) error {
|
||||||
|
duration = strings.TrimSpace(duration)
|
||||||
|
|
||||||
|
if duration == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
parsed, err := parseutil.ParseDurationSecond(duration)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
o.withMaxDuration = parsed
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// WithFileMode provides an Option to represent a file mode for a file sink.
|
||||||
|
// Supplying an empty string or whitespace will prevent this Option from being
|
||||||
// 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 {
|
||||||
// If supplied file mode is empty, just return early without setting anything.
|
// 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
|
// We can assume that this Option was called by something that didn't
|
||||||
// parse the incoming value, perhaps from a config map etc.
|
// parse the incoming value, perhaps from a config map etc.
|
||||||
mode = strings.TrimSpace(mode)
|
mode = strings.TrimSpace(mode)
|
||||||
if mode == "" {
|
if mode == "" {
|
||||||
@@ -176,70 +194,3 @@ func WithFileMode(mode string) Option {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithPrefix provides an option to represent a prefix for a file sink.
|
|
||||||
func WithPrefix(prefix string) Option {
|
|
||||||
return func(o *options) error {
|
|
||||||
o.withPrefix = prefix
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithSocketType provides an option to represent the socket type for a socket sink.
|
|
||||||
func WithSocketType(socketType string) Option {
|
|
||||||
return func(o *options) error {
|
|
||||||
socketType = strings.TrimSpace(socketType)
|
|
||||||
|
|
||||||
if socketType != "" {
|
|
||||||
o.withSocketType = socketType
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithMaxDuration provides an option to represent the max duration for writing to a socket sink.
|
|
||||||
func WithMaxDuration(duration string) Option {
|
|
||||||
return func(o *options) error {
|
|
||||||
duration = strings.TrimSpace(duration)
|
|
||||||
|
|
||||||
if duration == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
parsed, err := parseutil.ParseDurationSecond(duration)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
o.withMaxDuration = parsed
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -11,106 +11,6 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestOptions_WithFormat exercises WithFormat option to ensure it performs as expected.
|
|
||||||
func TestOptions_WithFormat(t *testing.T) {
|
|
||||||
tests := map[string]struct {
|
|
||||||
Value string
|
|
||||||
IsErrorExpected bool
|
|
||||||
ExpectedErrorMessage string
|
|
||||||
ExpectedValue auditFormat
|
|
||||||
}{
|
|
||||||
"empty": {
|
|
||||||
Value: "",
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "format cannot be empty",
|
|
||||||
},
|
|
||||||
"whitespace": {
|
|
||||||
Value: " ",
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "format cannot be empty",
|
|
||||||
},
|
|
||||||
"invalid-test": {
|
|
||||||
Value: "test",
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "event.(auditFormat).validate: 'test' is not a valid format: invalid parameter",
|
|
||||||
},
|
|
||||||
"valid-json": {
|
|
||||||
Value: "json",
|
|
||||||
IsErrorExpected: false,
|
|
||||||
ExpectedValue: AuditFormatJSON,
|
|
||||||
},
|
|
||||||
"valid-jsonx": {
|
|
||||||
Value: "jsonx",
|
|
||||||
IsErrorExpected: false,
|
|
||||||
ExpectedValue: AuditFormatJSONx,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, tc := range tests {
|
|
||||||
name := name
|
|
||||||
tc := tc
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
options := &options{}
|
|
||||||
applyOption := WithFormat(tc.Value)
|
|
||||||
err := applyOption(options)
|
|
||||||
switch {
|
|
||||||
case tc.IsErrorExpected:
|
|
||||||
require.Error(t, err)
|
|
||||||
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
|
||||||
default:
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tc.ExpectedValue, options.withFormat)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestOptions_WithSubtype exercises WithSubtype option to ensure it performs as expected.
|
|
||||||
func TestOptions_WithSubtype(t *testing.T) {
|
|
||||||
tests := map[string]struct {
|
|
||||||
Value string
|
|
||||||
IsErrorExpected bool
|
|
||||||
ExpectedErrorMessage string
|
|
||||||
ExpectedValue auditSubtype
|
|
||||||
}{
|
|
||||||
"empty": {
|
|
||||||
Value: "",
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "subtype cannot be empty",
|
|
||||||
},
|
|
||||||
"whitespace": {
|
|
||||||
Value: " ",
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "subtype cannot be empty",
|
|
||||||
},
|
|
||||||
"valid": {
|
|
||||||
Value: "AuditResponse",
|
|
||||||
IsErrorExpected: false,
|
|
||||||
ExpectedValue: AuditResponse,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, tc := range tests {
|
|
||||||
name := name
|
|
||||||
tc := tc
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
options := &options{}
|
|
||||||
applyOption := WithSubtype(tc.Value)
|
|
||||||
err := applyOption(options)
|
|
||||||
switch {
|
|
||||||
case tc.IsErrorExpected:
|
|
||||||
require.Error(t, err)
|
|
||||||
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
|
||||||
default:
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tc.ExpectedValue, options.withSubtype)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestOptions_WithNow exercises WithNow option to ensure it performs as expected.
|
// TestOptions_WithNow exercises WithNow option to ensure it performs as expected.
|
||||||
func TestOptions_WithNow(t *testing.T) {
|
func TestOptions_WithNow(t *testing.T) {
|
||||||
tests := map[string]struct {
|
tests := map[string]struct {
|
||||||
@@ -137,16 +37,16 @@ func TestOptions_WithNow(t *testing.T) {
|
|||||||
t.Run(name, func(t *testing.T) {
|
t.Run(name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
options := &options{}
|
opts := &options{}
|
||||||
applyOption := WithNow(tc.Value)
|
applyOption := WithNow(tc.Value)
|
||||||
err := applyOption(options)
|
err := applyOption(opts)
|
||||||
switch {
|
switch {
|
||||||
case tc.IsErrorExpected:
|
case tc.IsErrorExpected:
|
||||||
require.Error(t, err)
|
require.Error(t, err)
|
||||||
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
||||||
default:
|
default:
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.Equal(t, tc.ExpectedValue, options.withNow)
|
require.Equal(t, tc.ExpectedValue, opts.withNow)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -197,7 +97,103 @@ func TestOptions_WithID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestOptions_WithFacility exercises WithFacility option to ensure it performs as expected.
|
// TestOptions_Default exercises getDefaultOptions to assert the default values.
|
||||||
|
func TestOptions_Default(t *testing.T) {
|
||||||
|
opts := getDefaultOptions()
|
||||||
|
require.NotNil(t, opts)
|
||||||
|
require.True(t, time.Now().After(opts.withNow))
|
||||||
|
require.False(t, opts.withNow.IsZero())
|
||||||
|
require.Equal(t, "AUTH", opts.withFacility)
|
||||||
|
require.Equal(t, "vault", opts.withTag)
|
||||||
|
require.Equal(t, 2*time.Second, opts.withMaxDuration)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOptions_Opts exercises getOpts with various Option values.
|
||||||
|
func TestOptions_Opts(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
opts []Option
|
||||||
|
IsErrorExpected bool
|
||||||
|
ExpectedErrorMessage string
|
||||||
|
ExpectedID string
|
||||||
|
IsNowExpected bool
|
||||||
|
ExpectedNow time.Time
|
||||||
|
}{
|
||||||
|
"nil-options": {
|
||||||
|
opts: nil,
|
||||||
|
IsErrorExpected: false,
|
||||||
|
IsNowExpected: true,
|
||||||
|
},
|
||||||
|
"empty-options": {
|
||||||
|
opts: []Option{},
|
||||||
|
IsErrorExpected: false,
|
||||||
|
IsNowExpected: true,
|
||||||
|
},
|
||||||
|
"with-multiple-valid-id": {
|
||||||
|
opts: []Option{
|
||||||
|
WithID("qwerty"),
|
||||||
|
WithID("juan"),
|
||||||
|
},
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedID: "juan",
|
||||||
|
IsNowExpected: true,
|
||||||
|
},
|
||||||
|
"with-multiple-valid-now": {
|
||||||
|
opts: []Option{
|
||||||
|
WithNow(time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local)),
|
||||||
|
WithNow(time.Date(2023, time.July, 4, 13, 3, 0, 0, time.Local)),
|
||||||
|
},
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedNow: time.Date(2023, time.July, 4, 13, 3, 0, 0, time.Local),
|
||||||
|
IsNowExpected: false,
|
||||||
|
},
|
||||||
|
"with-multiple-valid-then-invalid-now": {
|
||||||
|
opts: []Option{
|
||||||
|
WithNow(time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local)),
|
||||||
|
WithNow(time.Time{}),
|
||||||
|
},
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "cannot specify 'now' to be the zero time instant",
|
||||||
|
},
|
||||||
|
"with-multiple-valid-options": {
|
||||||
|
opts: []Option{
|
||||||
|
WithID("qwerty"),
|
||||||
|
WithNow(time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local)),
|
||||||
|
},
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedID: "qwerty",
|
||||||
|
ExpectedNow: time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
opts, err := getOpts(tc.opts...)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case tc.IsErrorExpected:
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
||||||
|
default:
|
||||||
|
require.NotNil(t, opts)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.ExpectedID, opts.withID)
|
||||||
|
switch {
|
||||||
|
case tc.IsNowExpected:
|
||||||
|
require.True(t, time.Now().After(opts.withNow))
|
||||||
|
require.False(t, opts.withNow.IsZero())
|
||||||
|
default:
|
||||||
|
require.Equal(t, tc.ExpectedNow, opts.withNow)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestOptions_WithFacility exercises WithFacility Option to ensure it performs as expected.
|
||||||
func TestOptions_WithFacility(t *testing.T) {
|
func TestOptions_WithFacility(t *testing.T) {
|
||||||
tests := map[string]struct {
|
tests := map[string]struct {
|
||||||
Value string
|
Value string
|
||||||
@@ -235,7 +231,7 @@ func TestOptions_WithFacility(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestOptions_WithTag exercises WithTag option to ensure it performs as expected.
|
// TestOptions_WithTag exercises WithTag Option to ensure it performs as expected.
|
||||||
func TestOptions_WithTag(t *testing.T) {
|
func TestOptions_WithTag(t *testing.T) {
|
||||||
tests := map[string]struct {
|
tests := map[string]struct {
|
||||||
Value string
|
Value string
|
||||||
@@ -273,7 +269,7 @@ func TestOptions_WithTag(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestOptions_WithSocketType exercises WithSocketType option to ensure it performs as expected.
|
// TestOptions_WithSocketType exercises WithSocketType Option to ensure it performs as expected.
|
||||||
func TestOptions_WithSocketType(t *testing.T) {
|
func TestOptions_WithSocketType(t *testing.T) {
|
||||||
tests := map[string]struct {
|
tests := map[string]struct {
|
||||||
Value string
|
Value string
|
||||||
@@ -311,7 +307,7 @@ func TestOptions_WithSocketType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestOptions_WithMaxDuration exercises WithMaxDuration option to ensure it performs as expected.
|
// TestOptions_WithMaxDuration exercises WithMaxDuration Option to ensure it performs as expected.
|
||||||
func TestOptions_WithMaxDuration(t *testing.T) {
|
func TestOptions_WithMaxDuration(t *testing.T) {
|
||||||
tests := map[string]struct {
|
tests := map[string]struct {
|
||||||
Value string
|
Value string
|
||||||
@@ -365,7 +361,7 @@ func TestOptions_WithMaxDuration(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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 {
|
||||||
Value string
|
Value string
|
||||||
@@ -417,7 +413,7 @@ func TestOptions_WithFileMode(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
switch {
|
switch {
|
||||||
case tc.IsNilExpected:
|
case tc.IsNilExpected:
|
||||||
// Optional option 'not supplied' (i.e. was whitespace/empty string)
|
// Optional Option 'not supplied' (i.e. was whitespace/empty string)
|
||||||
require.Nil(t, options.withFileMode)
|
require.Nil(t, options.withFileMode)
|
||||||
default:
|
default:
|
||||||
// Dereference the pointer, so we can examine the file mode.
|
// Dereference the pointer, so we can examine the file mode.
|
||||||
@@ -427,125 +423,3 @@ func TestOptions_WithFileMode(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestOptions_Default exercises getDefaultOptions to assert the default values.
|
|
||||||
func TestOptions_Default(t *testing.T) {
|
|
||||||
opts := getDefaultOptions()
|
|
||||||
require.NotNil(t, opts)
|
|
||||||
require.True(t, time.Now().After(opts.withNow))
|
|
||||||
require.False(t, opts.withNow.IsZero())
|
|
||||||
require.Equal(t, "AUTH", opts.withFacility)
|
|
||||||
require.Equal(t, "vault", opts.withTag)
|
|
||||||
require.Equal(t, 2*time.Second, opts.withMaxDuration)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestOptions_Opts exercises getOpts with various Option values.
|
|
||||||
func TestOptions_Opts(t *testing.T) {
|
|
||||||
tests := map[string]struct {
|
|
||||||
opts []Option
|
|
||||||
IsErrorExpected bool
|
|
||||||
ExpectedErrorMessage string
|
|
||||||
ExpectedID string
|
|
||||||
ExpectedSubtype auditSubtype
|
|
||||||
ExpectedFormat auditFormat
|
|
||||||
IsNowExpected bool
|
|
||||||
ExpectedNow time.Time
|
|
||||||
}{
|
|
||||||
"nil-options": {
|
|
||||||
opts: nil,
|
|
||||||
IsErrorExpected: false,
|
|
||||||
IsNowExpected: true,
|
|
||||||
},
|
|
||||||
"empty-options": {
|
|
||||||
opts: []Option{},
|
|
||||||
IsErrorExpected: false,
|
|
||||||
IsNowExpected: true,
|
|
||||||
},
|
|
||||||
"with-multiple-valid-id": {
|
|
||||||
opts: []Option{
|
|
||||||
WithID("qwerty"),
|
|
||||||
WithID("juan"),
|
|
||||||
},
|
|
||||||
IsErrorExpected: false,
|
|
||||||
ExpectedID: "juan",
|
|
||||||
IsNowExpected: true,
|
|
||||||
},
|
|
||||||
"with-multiple-valid-subtype": {
|
|
||||||
opts: []Option{
|
|
||||||
WithSubtype("AuditRequest"),
|
|
||||||
WithSubtype("AuditResponse"),
|
|
||||||
},
|
|
||||||
IsErrorExpected: false,
|
|
||||||
ExpectedSubtype: AuditResponse,
|
|
||||||
IsNowExpected: true,
|
|
||||||
},
|
|
||||||
"with-multiple-valid-format": {
|
|
||||||
opts: []Option{
|
|
||||||
WithFormat("json"),
|
|
||||||
WithFormat("jsonx"),
|
|
||||||
},
|
|
||||||
IsErrorExpected: false,
|
|
||||||
ExpectedFormat: AuditFormatJSONx,
|
|
||||||
IsNowExpected: true,
|
|
||||||
},
|
|
||||||
"with-multiple-valid-now": {
|
|
||||||
opts: []Option{
|
|
||||||
WithNow(time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local)),
|
|
||||||
WithNow(time.Date(2023, time.July, 4, 13, 3, 0, 0, time.Local)),
|
|
||||||
},
|
|
||||||
IsErrorExpected: false,
|
|
||||||
ExpectedNow: time.Date(2023, time.July, 4, 13, 3, 0, 0, time.Local),
|
|
||||||
IsNowExpected: false,
|
|
||||||
},
|
|
||||||
"with-multiple-valid-then-invalid-now": {
|
|
||||||
opts: []Option{
|
|
||||||
WithNow(time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local)),
|
|
||||||
WithNow(time.Time{}),
|
|
||||||
},
|
|
||||||
IsErrorExpected: true,
|
|
||||||
ExpectedErrorMessage: "cannot specify 'now' to be the zero time instant",
|
|
||||||
},
|
|
||||||
"with-multiple-valid-options": {
|
|
||||||
opts: []Option{
|
|
||||||
WithID("qwerty"),
|
|
||||||
WithSubtype("AuditRequest"),
|
|
||||||
WithFormat("json"),
|
|
||||||
WithNow(time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local)),
|
|
||||||
},
|
|
||||||
IsErrorExpected: false,
|
|
||||||
ExpectedID: "qwerty",
|
|
||||||
ExpectedSubtype: AuditRequest,
|
|
||||||
ExpectedFormat: AuditFormatJSON,
|
|
||||||
ExpectedNow: time.Date(2023, time.July, 4, 12, 3, 0, 0, time.Local),
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, tc := range tests {
|
|
||||||
name := name
|
|
||||||
tc := tc
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
opts, err := getOpts(tc.opts...)
|
|
||||||
|
|
||||||
switch {
|
|
||||||
case tc.IsErrorExpected:
|
|
||||||
require.Error(t, err)
|
|
||||||
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
|
||||||
default:
|
|
||||||
require.NotNil(t, opts)
|
|
||||||
require.NoError(t, err)
|
|
||||||
require.Equal(t, tc.ExpectedID, opts.withID)
|
|
||||||
require.Equal(t, tc.ExpectedSubtype, opts.withSubtype)
|
|
||||||
require.Equal(t, tc.ExpectedFormat, opts.withFormat)
|
|
||||||
switch {
|
|
||||||
case tc.IsNowExpected:
|
|
||||||
require.True(t, time.Now().After(opts.withNow))
|
|
||||||
require.False(t, opts.withNow.IsZero())
|
|
||||||
default:
|
|
||||||
require.Equal(t, tc.ExpectedNow, opts.withNow)
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
226
internal/observability/event/sink_file.go
Normal file
226
internal/observability/event/sink_file.go
Normal file
@@ -0,0 +1,226 @@
|
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/hashicorp/eventlogger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// defaultFileMode is the default file permissions (read/write for everyone).
|
||||||
|
const (
|
||||||
|
defaultFileMode = 0o600
|
||||||
|
devnull = "/dev/null"
|
||||||
|
)
|
||||||
|
|
||||||
|
// FileSink is a sink node which handles writing events to file.
|
||||||
|
type FileSink struct {
|
||||||
|
file *os.File
|
||||||
|
fileLock sync.RWMutex
|
||||||
|
fileMode os.FileMode
|
||||||
|
path string
|
||||||
|
requiredFormat string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewFileSink should be used to create a new FileSink.
|
||||||
|
// Accepted options: WithFileMode.
|
||||||
|
func NewFileSink(path string, format string, opt ...Option) (*FileSink, error) {
|
||||||
|
const op = "event.NewFileSink"
|
||||||
|
|
||||||
|
// Parse and check path
|
||||||
|
p := strings.TrimSpace(path)
|
||||||
|
if p == "" {
|
||||||
|
return nil, fmt.Errorf("%s: path is required", op)
|
||||||
|
}
|
||||||
|
|
||||||
|
opts, err := getOpts(opt...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: error applying options: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
mode := os.FileMode(defaultFileMode)
|
||||||
|
// If we got an optional file mode supplied and our path isn't a special keyword
|
||||||
|
// then we should use the supplied file mode, or maintain the existing file mode.
|
||||||
|
switch {
|
||||||
|
case path == devnull:
|
||||||
|
case opts.withFileMode == nil:
|
||||||
|
case *opts.withFileMode == 0: // Maintain the existing file's mode when set to "0000".
|
||||||
|
fileInfo, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: unable to determine existing file mode: %w", op, err)
|
||||||
|
}
|
||||||
|
mode = fileInfo.Mode()
|
||||||
|
default:
|
||||||
|
mode = *opts.withFileMode
|
||||||
|
}
|
||||||
|
|
||||||
|
sink := &FileSink{
|
||||||
|
file: nil,
|
||||||
|
fileLock: sync.RWMutex{},
|
||||||
|
fileMode: mode,
|
||||||
|
requiredFormat: format,
|
||||||
|
path: p,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensure that the file can be successfully opened for writing;
|
||||||
|
// otherwise it will be too late to catch later without problems
|
||||||
|
// (ref: https://github.com/hashicorp/vault/issues/550)
|
||||||
|
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 sink, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process handles writing the event to the file sink.
|
||||||
|
func (f *FileSink) Process(ctx context.Context, e *eventlogger.Event) (*eventlogger.Event, error) {
|
||||||
|
const op = "event.(FileSink).Process"
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if e == nil {
|
||||||
|
return nil, fmt.Errorf("%s: event is nil: %w", op, ErrInvalidParameter)
|
||||||
|
}
|
||||||
|
|
||||||
|
// '/dev/null' path means we just do nothing and pretend we're done.
|
||||||
|
if f.path == devnull {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
formatted, found := e.Format(f.requiredFormat)
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("%s: unable to retrieve event formatted as %q", op, f.requiredFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
err := f.log(formatted)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: error writing file for sink: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// return nil for the event to indicate the pipeline is complete.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reopen handles closing and reopening the file.
|
||||||
|
func (f *FileSink) Reopen() error {
|
||||||
|
const op = "event.(FileSink).Reopen"
|
||||||
|
|
||||||
|
// '/dev/null' path means we just do nothing and pretend we're done.
|
||||||
|
if f.path == devnull {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
f.fileLock.Lock()
|
||||||
|
defer f.fileLock.Unlock()
|
||||||
|
|
||||||
|
if f.file == nil {
|
||||||
|
return f.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
err := f.file.Close()
|
||||||
|
// Set to nil here so that even if we error out, on the next access open() will be tried.
|
||||||
|
f.file = nil
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: unable to close file for re-opening on sink: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return f.open()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type describes the type of this node (sink).
|
||||||
|
func (_ *FileSink) Type() eventlogger.NodeType {
|
||||||
|
return eventlogger.NodeTypeSink
|
||||||
|
}
|
||||||
|
|
||||||
|
// open attempts to open a file at the sink's path, with the sink's fileMode permissions
|
||||||
|
// if one is not already open.
|
||||||
|
// It doesn't have any locking and relies on calling functions of FileSink to
|
||||||
|
// handle this (e.g. log and Reopen methods).
|
||||||
|
func (f *FileSink) open() error {
|
||||||
|
const op = "event.(FileSink).open"
|
||||||
|
|
||||||
|
if f.file != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.MkdirAll(filepath.Dir(f.path), f.fileMode); err != nil {
|
||||||
|
return fmt.Errorf("%s: unable to create file %q: %w", op, f.path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
f.file, err = os.OpenFile(f.path, os.O_APPEND|os.O_WRONLY|os.O_CREATE, f.fileMode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: unable to open file for sink: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change the file mode in case the log file already existed.
|
||||||
|
// We special case '/dev/null' since we can't chmod it, and bypass if the mode is zero.
|
||||||
|
switch f.path {
|
||||||
|
case devnull:
|
||||||
|
default:
|
||||||
|
if f.fileMode != 0 {
|
||||||
|
err = os.Chmod(f.path, f.fileMode)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: unable to change file %q permissions '%v' for sink: %w", op, f.path, f.fileMode, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// log writes the buffer to the file.
|
||||||
|
// It acquires a lock on the file to do this.
|
||||||
|
func (f *FileSink) log(data []byte) error {
|
||||||
|
const op = "event.(FileSink).log"
|
||||||
|
|
||||||
|
f.fileLock.Lock()
|
||||||
|
defer f.fileLock.Unlock()
|
||||||
|
|
||||||
|
reader := bytes.NewReader(data)
|
||||||
|
|
||||||
|
if err := f.open(); err != nil {
|
||||||
|
return fmt.Errorf("%s: unable to open file for sink: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := reader.WriteTo(f.file); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, opportunistically try to re-open the FD, once per call (1 retry attempt).
|
||||||
|
err := f.file.Close()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: unable to close file for sink: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
f.file = nil
|
||||||
|
|
||||||
|
if err := f.open(); err != nil {
|
||||||
|
return fmt.Errorf("%s: unable to re-open file for sink: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = reader.Seek(0, io.SeekStart)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: unable to seek to start of file for sink: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = reader.WriteTo(f.file)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("%s: unable to re-write to file for sink: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
301
internal/observability/event/sink_file_test.go
Normal file
301
internal/observability/event/sink_file_test.go
Normal file
@@ -0,0 +1,301 @@
|
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/hashicorp/vault/helper/namespace"
|
||||||
|
|
||||||
|
"github.com/hashicorp/eventlogger"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestFileSink_Type ensures that the node is a 'sink' type.
|
||||||
|
func TestFileSink_Type(t *testing.T) {
|
||||||
|
f, err := NewFileSink(filepath.Join(t.TempDir(), "vault.log"), "json")
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, f)
|
||||||
|
require.Equal(t, eventlogger.NodeTypeSink, f.Type())
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestNewFileSink tests creation of an AuditFileSink.
|
||||||
|
func TestNewFileSink(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
ShouldUseAbsolutePath bool // Path should contain the filename if temp dir is true
|
||||||
|
Path string
|
||||||
|
Format string
|
||||||
|
Options []Option
|
||||||
|
IsErrorExpected bool
|
||||||
|
ExpectedErrorMessage string
|
||||||
|
// Expected values of AuditFileSink
|
||||||
|
ExpectedFileMode os.FileMode
|
||||||
|
ExpectedFormat string
|
||||||
|
ExpectedPath string
|
||||||
|
ExpectedPrefix string
|
||||||
|
}{
|
||||||
|
"default-values": {
|
||||||
|
ShouldUseAbsolutePath: true,
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "event.NewFileSink: path is required",
|
||||||
|
},
|
||||||
|
"spacey-path": {
|
||||||
|
ShouldUseAbsolutePath: true,
|
||||||
|
Path: " ",
|
||||||
|
Format: "json",
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "event.NewFileSink: path is required",
|
||||||
|
},
|
||||||
|
"valid-path-and-format": {
|
||||||
|
Path: "vault.log",
|
||||||
|
Format: "json",
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedFileMode: defaultFileMode,
|
||||||
|
ExpectedFormat: "json",
|
||||||
|
ExpectedPrefix: "",
|
||||||
|
},
|
||||||
|
"file-mode-not-default-or-zero": {
|
||||||
|
Path: "vault.log",
|
||||||
|
Format: "json",
|
||||||
|
Options: []Option{WithFileMode("0007")},
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedFormat: "json",
|
||||||
|
ExpectedPrefix: "",
|
||||||
|
ExpectedFileMode: 0o007,
|
||||||
|
},
|
||||||
|
"prefix": {
|
||||||
|
Path: "vault.log",
|
||||||
|
Format: "json",
|
||||||
|
Options: []Option{WithFileMode("0007")},
|
||||||
|
IsErrorExpected: false,
|
||||||
|
ExpectedPrefix: "bleep",
|
||||||
|
ExpectedFormat: "json",
|
||||||
|
ExpectedFileMode: 0o007,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
// t.Parallel()
|
||||||
|
|
||||||
|
// If we need a real directory as a path we can use a temp dir.
|
||||||
|
// but we should keep track of it for comparison in the new sink.
|
||||||
|
var tempDir string
|
||||||
|
tempPath := tc.Path
|
||||||
|
if !tc.ShouldUseAbsolutePath {
|
||||||
|
tempDir = t.TempDir()
|
||||||
|
tempPath = filepath.Join(tempDir, tempPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
sink, err := NewFileSink(tempPath, tc.Format, tc.Options...)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case tc.IsErrorExpected:
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
||||||
|
require.Nil(t, sink)
|
||||||
|
default:
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, sink)
|
||||||
|
|
||||||
|
// Assert properties are correct.
|
||||||
|
require.Equal(t, tc.ExpectedFormat, sink.requiredFormat)
|
||||||
|
require.Equal(t, tc.ExpectedFileMode, sink.fileMode)
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case tc.ShouldUseAbsolutePath:
|
||||||
|
require.Equal(t, tc.ExpectedPath, sink.path)
|
||||||
|
default:
|
||||||
|
require.Equal(t, tempPath, sink.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFileSink_Reopen tests that the sink reopens files as expected when requested to.
|
||||||
|
// stdout and discard paths are ignored.
|
||||||
|
// see: https://developer.hashicorp.com/vault/docs/audit/file#file_path
|
||||||
|
func TestFileSink_Reopen(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
Path string
|
||||||
|
ShouldUseAbsolutePath bool
|
||||||
|
ShouldCreateFile bool
|
||||||
|
ShouldIgnoreFileMode bool
|
||||||
|
Options []Option
|
||||||
|
IsErrorExpected bool
|
||||||
|
ExpectedErrorMessage string
|
||||||
|
ExpectedFileMode os.FileMode
|
||||||
|
}{
|
||||||
|
// Should be ignored by Reopen
|
||||||
|
"devnull": {
|
||||||
|
Path: "/dev/null",
|
||||||
|
ShouldUseAbsolutePath: true,
|
||||||
|
ShouldIgnoreFileMode: true,
|
||||||
|
},
|
||||||
|
"happy": {
|
||||||
|
Path: "vault.log",
|
||||||
|
ExpectedFileMode: os.FileMode(defaultFileMode),
|
||||||
|
},
|
||||||
|
"filemode-existing": {
|
||||||
|
Path: "vault.log",
|
||||||
|
ShouldCreateFile: true,
|
||||||
|
Options: []Option{WithFileMode("0000")},
|
||||||
|
ExpectedFileMode: os.FileMode(defaultFileMode),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// If we need a real directory as a path we can use a temp dir.
|
||||||
|
// but we should keep track of it for comparison in the new sink.
|
||||||
|
var tempDir string
|
||||||
|
tempPath := tc.Path
|
||||||
|
if !tc.ShouldUseAbsolutePath {
|
||||||
|
tempDir = t.TempDir()
|
||||||
|
tempPath = filepath.Join(tempDir, tc.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the file mode is 0 then we will need a pre-created file to stat.
|
||||||
|
// Only do this for paths that are not 'special keywords'
|
||||||
|
if tc.ShouldCreateFile && tc.Path != devnull {
|
||||||
|
f, err := os.OpenFile(tempPath, os.O_CREATE, defaultFileMode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() {
|
||||||
|
err = os.Remove(f.Name())
|
||||||
|
require.NoError(t, err)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
sink, err := NewFileSink(tempPath, "json", tc.Options...)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, sink)
|
||||||
|
|
||||||
|
err = sink.Reopen()
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case tc.IsErrorExpected:
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
||||||
|
default:
|
||||||
|
require.NoError(t, err)
|
||||||
|
info, err := os.Stat(tempPath)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, info)
|
||||||
|
if !tc.ShouldIgnoreFileMode {
|
||||||
|
require.Equal(t, tc.ExpectedFileMode, info.Mode())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestFileSink_Process ensures that Process behaves as expected.
|
||||||
|
func TestFileSink_Process(t *testing.T) {
|
||||||
|
tests := map[string]struct {
|
||||||
|
ShouldUseAbsolutePath bool
|
||||||
|
Path string
|
||||||
|
ShouldCreateFile bool
|
||||||
|
Format string
|
||||||
|
ShouldIgnoreFormat bool
|
||||||
|
Data string
|
||||||
|
ShouldUseNilEvent bool
|
||||||
|
IsErrorExpected bool
|
||||||
|
ExpectedErrorMessage string
|
||||||
|
}{
|
||||||
|
"devnull": {
|
||||||
|
ShouldUseAbsolutePath: true,
|
||||||
|
Path: devnull,
|
||||||
|
Format: "json",
|
||||||
|
Data: "foo",
|
||||||
|
IsErrorExpected: false,
|
||||||
|
},
|
||||||
|
"no-formatted-data": {
|
||||||
|
ShouldCreateFile: true,
|
||||||
|
Path: "juan.log",
|
||||||
|
Format: "json",
|
||||||
|
Data: "foo",
|
||||||
|
ShouldIgnoreFormat: true,
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "event.(FileSink).Process: unable to retrieve event formatted as \"json\"",
|
||||||
|
},
|
||||||
|
"nil": {
|
||||||
|
Path: "foo.log",
|
||||||
|
Format: "json",
|
||||||
|
Data: "foo",
|
||||||
|
ShouldUseNilEvent: true,
|
||||||
|
IsErrorExpected: true,
|
||||||
|
ExpectedErrorMessage: "event.(FileSink).Process: event is nil: invalid parameter",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, tc := range tests {
|
||||||
|
name := name
|
||||||
|
tc := tc
|
||||||
|
t.Run(name, func(t *testing.T) {
|
||||||
|
// Temp dir for most testing unless we're trying to test an error
|
||||||
|
var tempDir string
|
||||||
|
tempPath := tc.Path
|
||||||
|
if !tc.ShouldUseAbsolutePath {
|
||||||
|
tempDir = t.TempDir()
|
||||||
|
tempPath = filepath.Join(tempDir, tc.Path)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a file if we will need it there before Process kicks off.
|
||||||
|
if tc.ShouldCreateFile && tc.Path != devnull {
|
||||||
|
f, err := os.OpenFile(tempPath, os.O_CREATE, defaultFileMode)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer func() {
|
||||||
|
err = os.Remove(f.Name())
|
||||||
|
require.NoError(t, err)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set up a sink
|
||||||
|
sink, err := NewFileSink(tempPath, tc.Format)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.NotNil(t, sink)
|
||||||
|
|
||||||
|
// Generate a fake event
|
||||||
|
ctx := namespace.RootContext(nil)
|
||||||
|
|
||||||
|
event := &eventlogger.Event{
|
||||||
|
Type: "audit",
|
||||||
|
CreatedAt: time.Now(),
|
||||||
|
Formatted: make(map[string][]byte),
|
||||||
|
Payload: struct{ ID string }{ID: "123"},
|
||||||
|
}
|
||||||
|
|
||||||
|
if !tc.ShouldIgnoreFormat {
|
||||||
|
event.FormattedAs(tc.Format, []byte(tc.Data))
|
||||||
|
}
|
||||||
|
|
||||||
|
if tc.ShouldUseNilEvent {
|
||||||
|
event = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The actual exercising of the sink.
|
||||||
|
event, err = sink.Process(ctx, event)
|
||||||
|
switch {
|
||||||
|
case tc.IsErrorExpected:
|
||||||
|
require.Error(t, err)
|
||||||
|
require.EqualError(t, err, tc.ExpectedErrorMessage)
|
||||||
|
require.Nil(t, event)
|
||||||
|
default:
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Nil(t, event)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
34
internal/observability/event/sink_noop.go
Normal file
34
internal/observability/event/sink_noop.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/hashicorp/eventlogger"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NoopSink is a sink node which handles ignores everything.
|
||||||
|
type NoopSink struct{}
|
||||||
|
|
||||||
|
// NewNoopSink should be used to create a new NoopSink.
|
||||||
|
func NewNoopSink() *NoopSink {
|
||||||
|
return &NoopSink{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process is a no-op and always returns nil event and nil error.
|
||||||
|
func (_ *NoopSink) Process(ctx context.Context, _ *eventlogger.Event) (*eventlogger.Event, error) {
|
||||||
|
// return nil for the event to indicate the pipeline is complete.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reopen is a no-op and always returns nil.
|
||||||
|
func (_ *NoopSink) Reopen() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type describes the type of this node (sink).
|
||||||
|
func (_ *NoopSink) Type() eventlogger.NodeType {
|
||||||
|
return eventlogger.NodeTypeSink
|
||||||
|
}
|
||||||
@@ -15,41 +15,41 @@ import (
|
|||||||
"github.com/hashicorp/eventlogger"
|
"github.com/hashicorp/eventlogger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuditSocketSink is a sink node which handles writing audit events to socket.
|
// SocketSink is a sink node which handles writing events to socket.
|
||||||
type AuditSocketSink struct {
|
type SocketSink struct {
|
||||||
format auditFormat
|
requiredFormat string
|
||||||
address string
|
address string
|
||||||
socketType string
|
socketType string
|
||||||
maxDuration time.Duration
|
maxDuration time.Duration
|
||||||
socketLock sync.RWMutex
|
socketLock sync.RWMutex
|
||||||
connection net.Conn
|
connection net.Conn
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuditSocketSink should be used to create a new AuditSocketSink.
|
// NewSocketSink should be used to create a new SocketSink.
|
||||||
// Accepted options: WithMaxDuration and WithSocketType.
|
// Accepted options: WithMaxDuration and WithSocketType.
|
||||||
func NewAuditSocketSink(format auditFormat, address string, opt ...Option) (*AuditSocketSink, error) {
|
func NewSocketSink(format string, address string, opt ...Option) (*SocketSink, error) {
|
||||||
const op = "event.NewAuditSocketSink"
|
const op = "event.NewSocketSink"
|
||||||
|
|
||||||
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, fmt.Errorf("%s: error applying options: %w", op, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
sink := &AuditSocketSink{
|
sink := &SocketSink{
|
||||||
format: format,
|
requiredFormat: format,
|
||||||
address: address,
|
address: address,
|
||||||
socketType: opts.withSocketType,
|
socketType: opts.withSocketType,
|
||||||
maxDuration: opts.withMaxDuration,
|
maxDuration: opts.withMaxDuration,
|
||||||
socketLock: sync.RWMutex{},
|
socketLock: sync.RWMutex{},
|
||||||
connection: nil,
|
connection: nil,
|
||||||
}
|
}
|
||||||
|
|
||||||
return sink, nil
|
return sink, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process handles writing the event to the socket.
|
// Process handles writing the event to the socket.
|
||||||
func (s *AuditSocketSink) 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.(AuditSocketSink).Process"
|
const op = "event.(SocketSink).Process"
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -64,9 +64,9 @@ func (s *AuditSocketSink) Process(ctx context.Context, e *eventlogger.Event) (*e
|
|||||||
return nil, fmt.Errorf("%s: event is nil: %w", op, ErrInvalidParameter)
|
return nil, fmt.Errorf("%s: event is nil: %w", op, ErrInvalidParameter)
|
||||||
}
|
}
|
||||||
|
|
||||||
formatted, found := e.Format(s.format.String())
|
formatted, found := e.Format(s.requiredFormat)
|
||||||
if !found {
|
if !found {
|
||||||
return nil, fmt.Errorf("%s: unable to retrieve event formatted as %q", op, s.format)
|
return nil, fmt.Errorf("%s: unable to retrieve event formatted as %q", op, s.requiredFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Try writing and return early if successful.
|
// Try writing and return early if successful.
|
||||||
@@ -95,8 +95,8 @@ func (s *AuditSocketSink) Process(ctx context.Context, e *eventlogger.Event) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reopen handles reopening the connection for the socket sink.
|
// Reopen handles reopening the connection for the socket sink.
|
||||||
func (s *AuditSocketSink) Reopen() error {
|
func (s *SocketSink) Reopen() error {
|
||||||
const op = "event.(AuditSocketSink).Reopen"
|
const op = "event.(SocketSink).Reopen"
|
||||||
|
|
||||||
s.socketLock.Lock()
|
s.socketLock.Lock()
|
||||||
defer s.socketLock.Unlock()
|
defer s.socketLock.Unlock()
|
||||||
@@ -110,13 +110,13 @@ func (s *AuditSocketSink) Reopen() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Type describes the type of this node (sink).
|
// Type describes the type of this node (sink).
|
||||||
func (s *AuditSocketSink) Type() eventlogger.NodeType {
|
func (_ *SocketSink) Type() eventlogger.NodeType {
|
||||||
return eventlogger.NodeTypeSink
|
return eventlogger.NodeTypeSink
|
||||||
}
|
}
|
||||||
|
|
||||||
// connect attempts to establish a connection using the socketType and address.
|
// connect attempts to establish a connection using the socketType and address.
|
||||||
func (s *AuditSocketSink) connect(ctx context.Context) error {
|
func (s *SocketSink) connect(ctx context.Context) error {
|
||||||
const op = "event.(AuditSocketSink).connect"
|
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 {
|
||||||
@@ -138,8 +138,8 @@ func (s *AuditSocketSink) 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 *AuditSocketSink) disconnect() error {
|
func (s *SocketSink) disconnect() error {
|
||||||
const op = "event.(AuditSocketSink).disconnect"
|
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 {
|
||||||
@@ -156,8 +156,8 @@ func (s *AuditSocketSink) 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 *AuditSocketSink) reconnect(ctx context.Context) error {
|
func (s *SocketSink) reconnect(ctx context.Context) error {
|
||||||
const op = "event.(AuditSocketSink).reconnect"
|
const op = "event.(SocketSink).reconnect"
|
||||||
|
|
||||||
err := s.disconnect()
|
err := s.disconnect()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -173,8 +173,8 @@ func (s *AuditSocketSink) 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 *AuditSocketSink) write(ctx context.Context, data []byte) error {
|
func (s *SocketSink) write(ctx context.Context, data []byte) error {
|
||||||
const op = "event.(AuditSocketSink).write"
|
const op = "event.(SocketSink).write"
|
||||||
|
|
||||||
// Ensure we're connected.
|
// Ensure we're connected.
|
||||||
err := s.connect(ctx)
|
err := s.connect(ctx)
|
||||||
66
internal/observability/event/sink_stdout.go
Normal file
66
internal/observability/event/sink_stdout.go
Normal file
@@ -0,0 +1,66 @@
|
|||||||
|
// Copyright (c) HashiCorp, Inc.
|
||||||
|
// SPDX-License-Identifier: MPL-2.0
|
||||||
|
|
||||||
|
package event
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/hashicorp/eventlogger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ eventlogger.Node = (*StdoutSink)(nil)
|
||||||
|
|
||||||
|
// StdoutSink is structure that implements the eventlogger.Node interface
|
||||||
|
// as a Sink node that writes the events to the standard output stream.
|
||||||
|
type StdoutSink struct {
|
||||||
|
requiredFormat string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewStdoutSinkNode creates a new StdoutSink that will persist the events
|
||||||
|
// it processes using the specified expected format.
|
||||||
|
func NewStdoutSinkNode(format string) *StdoutSink {
|
||||||
|
return &StdoutSink{
|
||||||
|
requiredFormat: format,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process persists the provided eventlogger.Event to the standard output stream.
|
||||||
|
func (n *StdoutSink) Process(ctx context.Context, event *eventlogger.Event) (*eventlogger.Event, error) {
|
||||||
|
const op = "event.(StdoutSink).Process"
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return nil, ctx.Err()
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
if event == nil {
|
||||||
|
return nil, fmt.Errorf("%s: event is nil: %w", op, ErrInvalidParameter)
|
||||||
|
}
|
||||||
|
|
||||||
|
formattedBytes, found := event.Format(n.requiredFormat)
|
||||||
|
if !found {
|
||||||
|
return nil, fmt.Errorf("%s: unable to retrieve event formatted as %q", op, n.requiredFormat)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := os.Stdout.Write(formattedBytes)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%s: error writing to stdout: %w", op, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Return nil, nil to indicate the pipeline is complete.
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reopen is a no-op for the StdoutSink type.
|
||||||
|
func (n *StdoutSink) Reopen() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type returns the eventlogger.NodeTypeSink constant.
|
||||||
|
func (n *StdoutSink) Type() eventlogger.NodeType {
|
||||||
|
return eventlogger.NodeTypeSink
|
||||||
|
}
|
||||||
@@ -12,16 +12,16 @@ import (
|
|||||||
"github.com/hashicorp/eventlogger"
|
"github.com/hashicorp/eventlogger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuditSyslogSink is a sink node which handles writing audit events to syslog.
|
// SyslogSink is a sink node which handles writing events to syslog.
|
||||||
type AuditSyslogSink struct {
|
type SyslogSink struct {
|
||||||
format auditFormat
|
requiredFormat string
|
||||||
logger gsyslog.Syslogger
|
logger gsyslog.Syslogger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewAuditSyslogSink should be used to create a new AuditSyslogSink.
|
// NewSyslogSink should be used to create a new SyslogSink.
|
||||||
// Accepted options: WithFacility and WithTag.
|
// Accepted options: WithFacility and WithTag.
|
||||||
func NewAuditSyslogSink(format auditFormat, opt ...Option) (*AuditSyslogSink, error) {
|
func NewSyslogSink(format string, opt ...Option) (*SyslogSink, error) {
|
||||||
const op = "event.NewAuditSyslogSink"
|
const op = "event.NewSyslogSink"
|
||||||
|
|
||||||
opts, err := getOpts(opt...)
|
opts, err := getOpts(opt...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -33,12 +33,12 @@ func NewAuditSyslogSink(format auditFormat, opt ...Option) (*AuditSyslogSink, er
|
|||||||
return nil, fmt.Errorf("%s: error creating syslogger: %w", op, err)
|
return nil, fmt.Errorf("%s: error creating syslogger: %w", op, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return &AuditSyslogSink{format: format, logger: logger}, nil
|
return &SyslogSink{requiredFormat: format, logger: logger}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Process handles writing the event to the syslog.
|
// Process handles writing the event to the syslog.
|
||||||
func (s *AuditSyslogSink) 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.(AuditSyslogSink).Process"
|
const op = "event.(SyslogSink).Process"
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
@@ -50,9 +50,9 @@ func (s *AuditSyslogSink) Process(ctx context.Context, e *eventlogger.Event) (*e
|
|||||||
return nil, fmt.Errorf("%s: event is nil: %w", op, ErrInvalidParameter)
|
return nil, fmt.Errorf("%s: event is nil: %w", op, ErrInvalidParameter)
|
||||||
}
|
}
|
||||||
|
|
||||||
formatted, found := e.Format(s.format.String())
|
formatted, found := e.Format(s.requiredFormat)
|
||||||
if !found {
|
if !found {
|
||||||
return nil, fmt.Errorf("%s: unable to retrieve event formatted as %q", op, s.format)
|
return nil, fmt.Errorf("%s: unable to retrieve event formatted as %q", op, s.requiredFormat)
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err := s.logger.Write(formatted)
|
_, err := s.logger.Write(formatted)
|
||||||
@@ -65,11 +65,11 @@ func (s *AuditSyslogSink) Process(ctx context.Context, e *eventlogger.Event) (*e
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Reopen is a no-op for a syslog sink.
|
// Reopen is a no-op for a syslog sink.
|
||||||
func (s *AuditSyslogSink) Reopen() error {
|
func (_ *SyslogSink) Reopen() error {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Type describes the type of this node (sink).
|
// Type describes the type of this node (sink).
|
||||||
func (s *AuditSyslogSink) Type() eventlogger.NodeType {
|
func (_ *SyslogSink) Type() eventlogger.NodeType {
|
||||||
return eventlogger.NodeTypeSink
|
return eventlogger.NodeTypeSink
|
||||||
}
|
}
|
||||||
@@ -1,28 +0,0 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package event
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
FileSink SinkType = "file"
|
|
||||||
SocketSink SinkType = "socket"
|
|
||||||
SyslogSink SinkType = "syslog"
|
|
||||||
)
|
|
||||||
|
|
||||||
// SinkType defines the type of sink
|
|
||||||
type SinkType string
|
|
||||||
|
|
||||||
// Validate ensures that SinkType is one of the set of allowed sink types.
|
|
||||||
func (t SinkType) Validate() error {
|
|
||||||
const op = "event.(SinkType).Validate"
|
|
||||||
switch t {
|
|
||||||
case FileSink, SocketSink, SyslogSink:
|
|
||||||
return nil
|
|
||||||
default:
|
|
||||||
return fmt.Errorf("%s: '%s' is not a valid sink type: %w", op, t, ErrInvalidParameter)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
// Copyright (c) HashiCorp, Inc.
|
|
||||||
// SPDX-License-Identifier: MPL-2.0
|
|
||||||
|
|
||||||
package event
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestSinkType_Validate exercises the validation for a sink type.
|
|
||||||
func TestSinkType_Validate(t *testing.T) {
|
|
||||||
tests := map[string]struct {
|
|
||||||
Value string
|
|
||||||
IsValid bool
|
|
||||||
ExpectedError string
|
|
||||||
}{
|
|
||||||
"file": {
|
|
||||||
Value: "file",
|
|
||||||
IsValid: true,
|
|
||||||
},
|
|
||||||
"syslog": {
|
|
||||||
Value: "syslog",
|
|
||||||
IsValid: true,
|
|
||||||
},
|
|
||||||
"socket": {
|
|
||||||
Value: "socket",
|
|
||||||
IsValid: true,
|
|
||||||
},
|
|
||||||
"empty": {
|
|
||||||
Value: "",
|
|
||||||
IsValid: false,
|
|
||||||
ExpectedError: "event.(SinkType).Validate: '' is not a valid sink type: invalid parameter",
|
|
||||||
},
|
|
||||||
"random": {
|
|
||||||
Value: "random",
|
|
||||||
IsValid: false,
|
|
||||||
ExpectedError: "event.(SinkType).Validate: 'random' is not a valid sink type: invalid parameter",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for name, tc := range tests {
|
|
||||||
name := name
|
|
||||||
tc := tc
|
|
||||||
t.Run(name, func(t *testing.T) {
|
|
||||||
t.Parallel()
|
|
||||||
sinkType := SinkType(tc.Value)
|
|
||||||
err := sinkType.Validate()
|
|
||||||
switch {
|
|
||||||
case tc.IsValid:
|
|
||||||
require.NoError(t, err)
|
|
||||||
case !tc.IsValid:
|
|
||||||
require.Error(t, err)
|
|
||||||
require.EqualError(t, err, tc.ExpectedError)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user