mirror of
				https://github.com/optim-enterprises-bv/vault.git
				synced 2025-10-31 02:28:09 +00:00 
			
		
		
		
	Audit: rewrite audit entry formatting to improve performance (#27952)
* rewrite audit entry formatting to improve performance
This commit is contained in:
		| @@ -244,7 +244,7 @@ func (n *noopWrapper) Process(ctx context.Context, e *eventlogger.Event) (*event | |||||||
| 	// formatted headers that would have made it to the logs via the sink node. | 	// formatted headers that would have made it to the logs via the sink node. | ||||||
| 	// They only appear in requests. | 	// They only appear in requests. | ||||||
| 	if a.Subtype == RequestType { | 	if a.Subtype == RequestType { | ||||||
| 		reqEntry := &RequestEntry{} | 		reqEntry := &Entry{} | ||||||
| 		err = json.Unmarshal(b, &reqEntry) | 		err = json.Unmarshal(b, &reqEntry) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return nil, fmt.Errorf("unable to parse formatted audit entry data: %w", err) | 			return nil, fmt.Errorf("unable to parse formatted audit entry data: %w", err) | ||||||
|   | |||||||
| @@ -5,8 +5,6 @@ package audit | |||||||
|  |  | ||||||
| import ( | import ( | ||||||
| 	"context" | 	"context" | ||||||
| 	"crypto/tls" |  | ||||||
| 	"errors" |  | ||||||
| 	"fmt" | 	"fmt" | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"runtime/debug" | 	"runtime/debug" | ||||||
| @@ -22,6 +20,7 @@ import ( | |||||||
| 	"github.com/hashicorp/vault/sdk/helper/salt" | 	"github.com/hashicorp/vault/sdk/helper/salt" | ||||||
| 	"github.com/hashicorp/vault/sdk/logical" | 	"github.com/hashicorp/vault/sdk/logical" | ||||||
| 	"github.com/jefferai/jsonx" | 	"github.com/jefferai/jsonx" | ||||||
|  | 	"github.com/mitchellh/copystructure" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| var _ eventlogger.Node = (*entryFormatter)(nil) | var _ eventlogger.Node = (*entryFormatter)(nil) | ||||||
| @@ -100,7 +99,7 @@ func (f *entryFormatter) Process(ctx context.Context, e *eventlogger.Event) (_ * | |||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	if a.Data == nil { | 	if a.Data == nil { | ||||||
| 		return nil, fmt.Errorf("cannot audit event (%s) with no data: %w", a.Subtype, ErrInvalidParameter) | 		return nil, fmt.Errorf("cannot audit a '%s' event with no data: %w", a.Subtype, ErrInvalidParameter) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Handle panics | 	// Handle panics | ||||||
| @@ -110,8 +109,13 @@ func (f *entryFormatter) Process(ctx context.Context, e *eventlogger.Event) (_ * | |||||||
| 			return | 			return | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
|  | 		path := "unknown" | ||||||
|  | 		if a.Data.Request != nil { | ||||||
|  | 			path = a.Data.Request.Path | ||||||
|  | 		} | ||||||
|  |  | ||||||
| 		f.logger.Error("panic during logging", | 		f.logger.Error("panic during logging", | ||||||
| 			"request_path", a.Data.Request.Path, | 			"request_path", path, | ||||||
| 			"audit_device_path", f.name, | 			"audit_device_path", f.name, | ||||||
| 			"error", r, | 			"error", r, | ||||||
| 			"stacktrace", string(debug.Stack())) | 			"stacktrace", string(debug.Stack())) | ||||||
| @@ -120,46 +124,12 @@ func (f *entryFormatter) Process(ctx context.Context, e *eventlogger.Event) (_ * | |||||||
| 		retErr = multierror.Append(retErr, fmt.Errorf("panic generating audit log: %q", f.name)).ErrorOrNil() | 		retErr = multierror.Append(retErr, fmt.Errorf("panic generating audit log: %q", f.name)).ErrorOrNil() | ||||||
| 	}() | 	}() | ||||||
|  |  | ||||||
| 	// Take a copy of the event data before we modify anything. | 	// Using 'any' to make exclusion easier, the JSON encoder doesn't care about types. | ||||||
| 	data, err := a.Data.Clone() |  | ||||||
| 	if err != nil { |  | ||||||
| 		return nil, fmt.Errorf("unable to clone audit event data: %w", err) |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// If the request is present in the input data, apply header configuration |  | ||||||
| 	// regardless. We shouldn't be in a situation where the header formatter isn't |  | ||||||
| 	// present as it's required. |  | ||||||
| 	if data.Request != nil { |  | ||||||
| 		// Ensure that any headers in the request, are formatted as required, and are |  | ||||||
| 		// only present if they have been configured to appear in the audit log. |  | ||||||
| 		// e.g. via: /sys/config/auditing/request-headers/:name |  | ||||||
| 		data.Request.Headers, err = f.config.headerFormatter.ApplyConfig(ctx, data.Request.Headers, f.salter) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, fmt.Errorf("unable to transform headers for auditing: %w", err) |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// If the request contains a Server-Side Consistency Token (SSCT), and we |  | ||||||
| 	// have an auth response, overwrite the existing client token with the SSCT, |  | ||||||
| 	// so that the SSCT appears in the audit log for this entry. |  | ||||||
| 	if data.Request != nil && data.Request.InboundSSCToken != "" && data.Auth != nil { |  | ||||||
| 		data.Auth.ClientToken = data.Request.InboundSSCToken |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// Using 'any' as we have two different types that we can get back from either |  | ||||||
| 	// formatRequest or formatResponse, but the JSON encoder doesn't care about types. |  | ||||||
| 	var entry any | 	var entry any | ||||||
|  | 	var err error | ||||||
| 	switch a.Subtype { | 	entry, err = f.createEntry(ctx, a) | ||||||
| 	case RequestType: |  | ||||||
| 		entry, err = f.formatRequest(ctx, data, a) |  | ||||||
| 	case ResponseType: |  | ||||||
| 		entry, err = f.formatResponse(ctx, data, a) |  | ||||||
| 	default: |  | ||||||
| 		return nil, fmt.Errorf("unknown audit event subtype: %q", a.Subtype) |  | ||||||
| 	} |  | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		return nil, fmt.Errorf("unable to parse %s from audit event: %w", a.Subtype, err) | 		return nil, err | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// If this pipeline has been configured with (Enterprise-only) exclusions then | 	// If this pipeline has been configured with (Enterprise-only) exclusions then | ||||||
| @@ -197,21 +167,12 @@ func (f *entryFormatter) Process(ctx context.Context, e *eventlogger.Event) (_ * | |||||||
| 		result = append([]byte(f.config.prefix), result...) | 		result = append([]byte(f.config.prefix), result...) | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	// Copy some properties from the event (and audit event) and store the | 	// Create a new event, so we can store our formatted data without conflict. | ||||||
| 	// format for the next (sink) node to Process. |  | ||||||
| 	a2 := &AuditEvent{ |  | ||||||
| 		ID:        a.ID, |  | ||||||
| 		Version:   a.Version, |  | ||||||
| 		Subtype:   a.Subtype, |  | ||||||
| 		Timestamp: a.Timestamp, |  | ||||||
| 		Data:      data, // Use the cloned data here rather than a pointer to the original. |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	e2 := &eventlogger.Event{ | 	e2 := &eventlogger.Event{ | ||||||
| 		Type:      e.Type, | 		Type:      e.Type, | ||||||
| 		CreatedAt: e.CreatedAt, | 		CreatedAt: e.CreatedAt, | ||||||
| 		Formatted: make(map[string][]byte), // we are about to set this ourselves. | 		Formatted: make(map[string][]byte), // we are about to set this ourselves. | ||||||
| 		Payload:   a2, | 		Payload:   a, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	e2.FormattedAs(f.config.requiredFormat.String(), result) | 	e2.FormattedAs(f.config.requiredFormat.String(), result) | ||||||
| @@ -219,377 +180,35 @@ func (f *entryFormatter) Process(ctx context.Context, e *eventlogger.Event) (_ * | |||||||
| 	return e2, nil | 	return e2, nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // formatRequest attempts to format the specified logical.LogInput into a RequestEntry. | // remoteAddr safely gets the remote address avoiding a nil pointer. | ||||||
| func (f *entryFormatter) formatRequest(ctx context.Context, in *logical.LogInput, provider timeProvider) (*RequestEntry, error) { | func remoteAddr(req *logical.Request) string { | ||||||
| 	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") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// 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 { |  | ||||||
| 		var err error |  | ||||||
| 		err = hashAuth(ctx, f.salter, auth, f.config.hmacAccessor) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		err = hashRequest(ctx, f.salter, 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 req.HTTPRequest != nil && req.HTTPRequest.RequestURI != req.Path { |  | ||||||
| 		reqEntry.Request.RequestURI = req.HTTPRequest.RequestURI |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	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 { |  | ||||||
| 		// Use the time provider to supply the time for this entry. |  | ||||||
| 		reqEntry.Time = provider.formattedTime() |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return reqEntry, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // formatResponse attempts to format the specified logical.LogInput into a ResponseEntry. |  | ||||||
| func (f *entryFormatter) formatResponse(ctx context.Context, in *logical.LogInput, provider timeProvider) (*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") |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	// 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 { |  | ||||||
| 		var err error |  | ||||||
| 		err = hashAuth(ctx, f.salter, auth, f.config.hmacAccessor) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		err = hashRequest(ctx, f.salter, req, f.config.hmacAccessor, in.NonHMACReqDataKeys) |  | ||||||
| 		if err != nil { |  | ||||||
| 			return nil, err |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		err = hashResponse(ctx, f.salter, 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 req.HTTPRequest != nil && req.HTTPRequest.RequestURI != req.Path { |  | ||||||
| 		respEntry.Request.RequestURI = req.HTTPRequest.RequestURI |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	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 { |  | ||||||
| 		// Use the time provider to supply the time for this entry. |  | ||||||
| 		respEntry.Time = provider.formattedTime() |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	return respEntry, nil |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // getRemoteAddr safely gets the remote address avoiding a nil pointer |  | ||||||
| func getRemoteAddr(req *logical.Request) string { |  | ||||||
| 	if req != nil && req.Connection != nil { | 	if req != nil && req.Connection != nil { | ||||||
| 		return req.Connection.RemoteAddr | 		return req.Connection.RemoteAddr | ||||||
| 	} | 	} | ||||||
| 	return "" | 	return "" | ||||||
| } | } | ||||||
|  |  | ||||||
| // getRemotePort safely gets the remote port avoiding a nil pointer | // remotePort safely gets the remote port avoiding a nil pointer. | ||||||
| func getRemotePort(req *logical.Request) int { | func remotePort(req *logical.Request) int { | ||||||
| 	if req != nil && req.Connection != nil { | 	if req != nil && req.Connection != nil { | ||||||
| 		return req.Connection.RemotePort | 		return req.Connection.RemotePort | ||||||
| 	} | 	} | ||||||
| 	return 0 | 	return 0 | ||||||
| } | } | ||||||
|  |  | ||||||
| // getClientCertificateSerialNumber attempts the retrieve the serial number of | // clientCertSerialNumber attempts the retrieve the serial number of the peer | ||||||
| // the peer certificate from the specified tls.ConnectionState. | // certificate from the specified tls.ConnectionState. | ||||||
| func getClientCertificateSerialNumber(connState *tls.ConnectionState) string { | func clientCertSerialNumber(req *logical.Request) string { | ||||||
|  | 	if req == nil { | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if req.Connection == nil { | ||||||
|  | 		return "" | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	connState := req.Connection.ConnState | ||||||
|  |  | ||||||
| 	if connState == nil || len(connState.VerifiedChains) == 0 || len(connState.VerifiedChains[0]) == 0 { | 	if connState == nil || len(connState.VerifiedChains) == 0 || len(connState.VerifiedChains[0]) == 0 { | ||||||
| 		return "" | 		return "" | ||||||
| 	} | 	} | ||||||
| @@ -617,25 +236,6 @@ func parseVaultTokenFromJWT(token string) *string { | |||||||
| 	return &claims.ID | 	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) |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 	} |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // newTemporaryEntryFormatter creates a cloned entryFormatter instance with a non-persistent Salter. | // newTemporaryEntryFormatter creates a cloned entryFormatter instance with a non-persistent Salter. | ||||||
| func newTemporaryEntryFormatter(n *entryFormatter) *entryFormatter { | func newTemporaryEntryFormatter(n *entryFormatter) *entryFormatter { | ||||||
| 	return &entryFormatter{ | 	return &entryFormatter{ | ||||||
| @@ -648,3 +248,369 @@ func newTemporaryEntryFormatter(n *entryFormatter) *entryFormatter { | |||||||
| func (s *nonPersistentSalt) Salt(_ context.Context) (*salt.Salt, error) { | func (s *nonPersistentSalt) Salt(_ context.Context) (*salt.Salt, error) { | ||||||
| 	return salt.NewNonpersistentSalt(), nil | 	return salt.NewNonpersistentSalt(), nil | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // clone can be used to deep clone the specified type. | ||||||
|  | func clone[V any](s V) (V, error) { | ||||||
|  | 	s2, err := copystructure.Copy(s) | ||||||
|  |  | ||||||
|  | 	return s2.(V), err | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // newAuth takes a logical.Auth and the number of remaining client token uses | ||||||
|  | // (which should be supplied from the logical.Request's client token), and creates | ||||||
|  | // an audit Auth. | ||||||
|  | // tokenRemainingUses should be the client token remaining uses to include in auth. | ||||||
|  | // This usually can be found in logical.Request.ClientTokenRemainingUses. | ||||||
|  | // NOTE: supplying a nil value for auth will result in a nil return value and error. | ||||||
|  | // The caller should check the return value before attempting to use it. | ||||||
|  | func newAuth(auth *logical.Auth, tokenRemainingUses int) (*Auth, error) { | ||||||
|  | 	if auth == nil { | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	extNSPolicies, err := clone(auth.ExternalNamespacePolicies) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("unable to clone logical auth: external namespace policies: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	identityPolicies, err := clone(auth.IdentityPolicies) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("unable to clone logical auth: identity policies: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	metadata, err := clone(auth.Metadata) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("unable to clone logical auth: metadata: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	policies, err := clone(auth.Policies) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("unable to clone logical auth: policies: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var policyResults *PolicyResults | ||||||
|  | 	if auth.PolicyResults != nil { | ||||||
|  | 		policyResults = &PolicyResults{ | ||||||
|  | 			Allowed:          auth.PolicyResults.Allowed, | ||||||
|  | 			GrantingPolicies: make([]PolicyInfo, len(auth.PolicyResults.GrantingPolicies)), | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		for _, p := range auth.PolicyResults.GrantingPolicies { | ||||||
|  | 			policyResults.GrantingPolicies = append(policyResults.GrantingPolicies, PolicyInfo{ | ||||||
|  | 				Name:          p.Name, | ||||||
|  | 				NamespaceId:   p.NamespaceId, | ||||||
|  | 				NamespacePath: p.NamespacePath, | ||||||
|  | 				Type:          p.Type, | ||||||
|  | 			}) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	tokenPolicies, err := clone(auth.TokenPolicies) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("unable to clone logical auth: token policies: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var tokenIssueTime string | ||||||
|  | 	if !auth.IssueTime.IsZero() { | ||||||
|  | 		tokenIssueTime = auth.IssueTime.Format(time.RFC3339) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &Auth{ | ||||||
|  | 		Accessor:                  auth.Accessor, | ||||||
|  | 		ClientToken:               auth.ClientToken, | ||||||
|  | 		DisplayName:               auth.DisplayName, | ||||||
|  | 		EntityCreated:             auth.EntityCreated, | ||||||
|  | 		EntityID:                  auth.EntityID, | ||||||
|  | 		ExternalNamespacePolicies: extNSPolicies, | ||||||
|  | 		IdentityPolicies:          identityPolicies, | ||||||
|  | 		Metadata:                  metadata, | ||||||
|  | 		NoDefaultPolicy:           auth.NoDefaultPolicy, | ||||||
|  | 		NumUses:                   auth.NumUses, | ||||||
|  | 		Policies:                  policies, | ||||||
|  | 		PolicyResults:             policyResults, | ||||||
|  | 		RemainingUses:             tokenRemainingUses, | ||||||
|  | 		TokenPolicies:             tokenPolicies, | ||||||
|  | 		TokenIssueTime:            tokenIssueTime, | ||||||
|  | 		TokenTTL:                  int64(auth.TTL.Seconds()), | ||||||
|  | 		TokenType:                 auth.TokenType.String(), | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // newRequest takes a logical.Request and namespace.Namespace, transforms and | ||||||
|  | // aggregates them into an audit Request. | ||||||
|  | func newRequest(req *logical.Request, ns *namespace.Namespace) (*Request, error) { | ||||||
|  | 	if req == nil { | ||||||
|  | 		return nil, fmt.Errorf("request cannot be nil") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	remoteAddr := remoteAddr(req) | ||||||
|  | 	remotePort := remotePort(req) | ||||||
|  | 	clientCertSerial := clientCertSerialNumber(req) | ||||||
|  |  | ||||||
|  | 	data, err := clone(req.Data) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("unable to clone logical request: data: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	headers, err := clone(req.Headers) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("unable to clone logical request: headers: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var reqURI string | ||||||
|  | 	if req.HTTPRequest != nil && req.HTTPRequest.RequestURI != req.Path { | ||||||
|  | 		reqURI = req.HTTPRequest.RequestURI | ||||||
|  | 	} | ||||||
|  | 	var wrapTTL int | ||||||
|  | 	if req.WrapInfo != nil { | ||||||
|  | 		wrapTTL = int(req.WrapInfo.TTL / time.Second) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &Request{ | ||||||
|  | 		ClientCertificateSerialNumber: clientCertSerial, | ||||||
|  | 		ClientID:                      req.ClientID, | ||||||
|  | 		ClientToken:                   req.ClientToken, | ||||||
|  | 		ClientTokenAccessor:           req.ClientTokenAccessor, | ||||||
|  | 		Data:                          data, | ||||||
|  | 		Headers:                       headers, | ||||||
|  | 		ID:                            req.ID, | ||||||
|  | 		MountAccessor:                 req.MountAccessor, | ||||||
|  | 		MountClass:                    req.MountClass(), | ||||||
|  | 		MountIsExternalPlugin:         req.MountIsExternalPlugin(), | ||||||
|  | 		MountPoint:                    req.MountPoint, | ||||||
|  | 		MountRunningSha256:            req.MountRunningSha256(), | ||||||
|  | 		MountRunningVersion:           req.MountRunningVersion(), | ||||||
|  | 		MountType:                     req.MountType, | ||||||
|  | 		Namespace: &Namespace{ | ||||||
|  | 			ID:   ns.ID, | ||||||
|  | 			Path: ns.Path, | ||||||
|  | 		}, | ||||||
|  | 		Operation:          req.Operation, | ||||||
|  | 		Path:               req.Path, | ||||||
|  | 		PolicyOverride:     req.PolicyOverride, | ||||||
|  | 		RemoteAddr:         remoteAddr, | ||||||
|  | 		RemotePort:         remotePort, | ||||||
|  | 		ReplicationCluster: req.ReplicationCluster, | ||||||
|  | 		RequestURI:         reqURI, | ||||||
|  | 		WrapTTL:            wrapTTL, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // newResponse takes a logical.Response and logical.Request, transforms and | ||||||
|  | // aggregates them into an audit Response. | ||||||
|  | // isElisionRequired is used to indicate that response 'Data' should be elided. | ||||||
|  | func newResponse(resp *logical.Response, req *logical.Request, isElisionRequired bool) (*Response, error) { | ||||||
|  | 	if resp == nil { | ||||||
|  | 		return nil, nil | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if req == nil { | ||||||
|  | 		// Request should never be nil, even for a response. | ||||||
|  | 		return nil, fmt.Errorf("request cannot be nil") | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	auth, err := newAuth(resp.Auth, req.ClientTokenRemainingUses) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("unable to convert logical auth response: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var data map[string]any | ||||||
|  | 	if resp.Data != nil { | ||||||
|  | 		data = make(map[string]any, len(resp.Data)) | ||||||
|  |  | ||||||
|  | 		if isElisionRequired { | ||||||
|  | 			// Performs the actual elision (ideally for list operations) of response data, | ||||||
|  | 			// once surrounding code has determined it should apply to a particular request. | ||||||
|  | 			// If the value for a key should not be elided, then it will be cloned. | ||||||
|  | 			for k, v := range resp.Data { | ||||||
|  | 				isCloneRequired := true | ||||||
|  | 				switch k { | ||||||
|  | 				case "keys": | ||||||
|  | 					if vSlice, ok := v.([]string); ok { | ||||||
|  | 						data[k] = len(vSlice) | ||||||
|  | 						isCloneRequired = false | ||||||
|  | 					} | ||||||
|  | 				case "key_info": | ||||||
|  | 					if vMap, ok := v.(map[string]any); ok { | ||||||
|  | 						data[k] = len(vMap) | ||||||
|  | 						isCloneRequired = false | ||||||
|  | 					} | ||||||
|  | 				} | ||||||
|  |  | ||||||
|  | 				// Clone values if they weren't legitimate keys or key_info. | ||||||
|  | 				if isCloneRequired { | ||||||
|  | 					v2, err := clone(v) | ||||||
|  | 					if err != nil { | ||||||
|  | 						return nil, fmt.Errorf("unable to clone response data while eliding: %w", err) | ||||||
|  | 					} | ||||||
|  | 					data[k] = v2 | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			// Deep clone all values, no shortcuts here. | ||||||
|  | 			data, err = clone(resp.Data) | ||||||
|  | 			if err != nil { | ||||||
|  | 				return nil, fmt.Errorf("unable to clone response data: %w", err) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	headers, err := clone(resp.Headers) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("unable to clone logical response: headers: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var secret *Secret | ||||||
|  | 	if resp.Secret != nil { | ||||||
|  | 		secret = &Secret{LeaseID: resp.Secret.LeaseID} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var wrapInfo *ResponseWrapInfo | ||||||
|  | 	if resp.WrapInfo != nil { | ||||||
|  | 		token := resp.WrapInfo.Token | ||||||
|  | 		if jwtToken := parseVaultTokenFromJWT(token); jwtToken != nil { | ||||||
|  | 			token = *jwtToken | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		ttl := int(resp.WrapInfo.TTL / time.Second) | ||||||
|  | 		wrapInfo = &ResponseWrapInfo{ | ||||||
|  | 			TTL:             ttl, | ||||||
|  | 			Token:           token, | ||||||
|  | 			Accessor:        resp.WrapInfo.Accessor, | ||||||
|  | 			CreationTime:    resp.WrapInfo.CreationTime.UTC().Format(time.RFC3339Nano), | ||||||
|  | 			CreationPath:    resp.WrapInfo.CreationPath, | ||||||
|  | 			WrappedAccessor: resp.WrapInfo.WrappedAccessor, | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	warnings, err := clone(resp.Warnings) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("unable to clone logical response: warnings: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return &Response{ | ||||||
|  | 		Auth:                  auth, | ||||||
|  | 		Data:                  data, | ||||||
|  | 		Headers:               headers, | ||||||
|  | 		MountAccessor:         req.MountAccessor, | ||||||
|  | 		MountClass:            req.MountClass(), | ||||||
|  | 		MountIsExternalPlugin: req.MountIsExternalPlugin(), | ||||||
|  | 		MountPoint:            req.MountPoint, | ||||||
|  | 		MountRunningSha256:    req.MountRunningSha256(), | ||||||
|  | 		MountRunningVersion:   req.MountRunningVersion(), | ||||||
|  | 		MountType:             req.MountType, | ||||||
|  | 		Redirect:              resp.Redirect, | ||||||
|  | 		Secret:                secret, | ||||||
|  | 		WrapInfo:              wrapInfo, | ||||||
|  | 		Warnings:              warnings, | ||||||
|  | 	}, nil | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // createEntry takes the AuditEvent and builds an audit Entry. | ||||||
|  | // The Entry will be HMAC'd and elided where required. | ||||||
|  | func (f *entryFormatter) createEntry(ctx context.Context, a *AuditEvent) (*Entry, error) { | ||||||
|  | 	select { | ||||||
|  | 	case <-ctx.Done(): | ||||||
|  | 		return nil, ctx.Err() | ||||||
|  | 	default: | ||||||
|  |  | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	data := a.Data | ||||||
|  |  | ||||||
|  | 	if data.Request == nil { | ||||||
|  | 		// Request should never be nil, even for a response. | ||||||
|  | 		return nil, fmt.Errorf("unable to parse request from '%s' audit event: request cannot be nil", a.Subtype) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	ns, err := namespace.FromContext(ctx) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("unable to retrieve namespace from context: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	auth, err := newAuth(data.Auth, data.Request.ClientTokenRemainingUses) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("cannot convert auth: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	req, err := newRequest(data.Request, ns) | ||||||
|  | 	if err != nil { | ||||||
|  | 		return nil, fmt.Errorf("cannot convert request: %w", err) | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var resp *Response | ||||||
|  | 	if a.Subtype == ResponseType { | ||||||
|  | 		shouldElide := f.config.elideListResponses && req.Operation == logical.ListOperation | ||||||
|  | 		resp, err = newResponse(data.Response, data.Request, shouldElide) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("cannot convert response: %w", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	var outerErr string | ||||||
|  | 	if data.OuterErr != nil { | ||||||
|  | 		outerErr = data.OuterErr.Error() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	entryType := data.Type | ||||||
|  | 	if entryType == "" { | ||||||
|  | 		entryType = a.Subtype.String() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	entry := &Entry{ | ||||||
|  | 		Auth:          auth, | ||||||
|  | 		Error:         outerErr, | ||||||
|  | 		Forwarded:     false, | ||||||
|  | 		ForwardedFrom: data.Request.ForwardedFrom, | ||||||
|  | 		Request:       req, | ||||||
|  | 		Response:      resp, | ||||||
|  | 		Type:          entryType, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	if !f.config.omitTime { | ||||||
|  | 		// Use the time provider to supply the time for this entry. | ||||||
|  | 		entry.Time = a.timeProvider().formattedTime() | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// If the request is present in the input data, apply header configuration | ||||||
|  | 	// regardless. We shouldn't be in a situation where the header formatter isn't | ||||||
|  | 	// present as it's required. | ||||||
|  | 	if entry.Request != nil { | ||||||
|  | 		// Ensure that any headers in the request, are formatted as required, and are | ||||||
|  | 		// only present if they have been configured to appear in the audit log. | ||||||
|  | 		// e.g. via: /sys/config/auditing/request-headers/:name | ||||||
|  | 		entry.Request.Headers, err = f.config.headerFormatter.ApplyConfig(ctx, entry.Request.Headers, f.salter) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, fmt.Errorf("unable to transform headers for auditing: %w", err) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// If the request contains a Server-Side Consistency Token (SSCT), and we | ||||||
|  | 	// have an auth response, overwrite the existing client token with the SSCT, | ||||||
|  | 	// so that the SSCT appears in the audit log for this entry. | ||||||
|  | 	if data.Request != nil && data.Request.InboundSSCToken != "" && entry.Auth != nil { | ||||||
|  | 		entry.Auth.ClientToken = data.Request.InboundSSCToken | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	// Hash the entry if we aren't expecting raw output. | ||||||
|  | 	if !f.config.raw { | ||||||
|  | 		// Requests and responses have auth and request. | ||||||
|  | 		err = hashAuth(ctx, f.salter, entry.Auth, f.config.hmacAccessor) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		err = hashRequest(ctx, f.salter, entry.Request, f.config.hmacAccessor, data.NonHMACReqDataKeys) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return nil, err | ||||||
|  | 		} | ||||||
|  |  | ||||||
|  | 		if a.Subtype == ResponseType { | ||||||
|  | 			if err = hashResponse(ctx, f.salter, entry.Response, f.config.hmacAccessor, data.NonHMACRespDataKeys); err != nil { | ||||||
|  | 				return nil, err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return entry, nil | ||||||
|  | } | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ import ( | |||||||
| 	"github.com/hashicorp/go-hclog" | 	"github.com/hashicorp/go-hclog" | ||||||
| 	"github.com/hashicorp/go-sockaddr" | 	"github.com/hashicorp/go-sockaddr" | ||||||
| 	"github.com/hashicorp/vault/helper/namespace" | 	"github.com/hashicorp/vault/helper/namespace" | ||||||
|  | 	"github.com/hashicorp/vault/helper/testhelpers/corehelpers" | ||||||
| 	"github.com/hashicorp/vault/internal/observability/event" | 	"github.com/hashicorp/vault/internal/observability/event" | ||||||
| 	"github.com/hashicorp/vault/sdk/helper/jsonutil" | 	"github.com/hashicorp/vault/sdk/helper/jsonutil" | ||||||
| 	"github.com/hashicorp/vault/sdk/helper/salt" | 	"github.com/hashicorp/vault/sdk/helper/salt" | ||||||
| @@ -244,7 +245,7 @@ func TestEntryFormatter_Type(t *testing.T) { | |||||||
| } | } | ||||||
|  |  | ||||||
| // TestEntryFormatter_Process attempts to run the Process method to convert the | // TestEntryFormatter_Process attempts to run the Process method to convert the | ||||||
| // logical.LogInput within an audit event to JSON and JSONx (RequestEntry or ResponseEntry). | // logical.LogInput within an audit event to JSON and JSONx (Entry), | ||||||
| func TestEntryFormatter_Process(t *testing.T) { | func TestEntryFormatter_Process(t *testing.T) { | ||||||
| 	t.Parallel() | 	t.Parallel() | ||||||
|  |  | ||||||
| @@ -258,42 +259,43 @@ func TestEntryFormatter_Process(t *testing.T) { | |||||||
| 	}{ | 	}{ | ||||||
| 		"json-request-no-data": { | 		"json-request-no-data": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "cannot audit event (request) with no data: invalid internal parameter", | 			ExpectedErrorMessage: "cannot audit a 'request' event with no data: invalid internal parameter", | ||||||
| 			Subtype:              RequestType, | 			Subtype:              RequestType, | ||||||
| 			RequiredFormat:       JSONFormat, | 			RequiredFormat:       JSONFormat, | ||||||
| 			Data:                 nil, | 			Data:                 nil, | ||||||
| 		}, | 		}, | ||||||
| 		"json-response-no-data": { | 		"json-response-no-data": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "cannot audit event (response) with no data: invalid internal parameter", | 			ExpectedErrorMessage: "cannot audit a 'response' event with no data: invalid internal parameter", | ||||||
| 			Subtype:              ResponseType, | 			Subtype:              ResponseType, | ||||||
| 			RequiredFormat:       JSONFormat, | 			RequiredFormat:       JSONFormat, | ||||||
| 			Data:                 nil, | 			Data:                 nil, | ||||||
| 		}, | 		}, | ||||||
| 		"json-request-basic-input": { | 		"json-request-basic-input": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "unable to parse request from audit event: request to request-audit a nil request", | 			ExpectedErrorMessage: "unable to parse request from 'request' audit event: request cannot be nil", | ||||||
| 			Subtype:              RequestType, | 			Subtype:              RequestType, | ||||||
| 			RequiredFormat:       JSONFormat, | 			RequiredFormat:       JSONFormat, | ||||||
| 			Data:                 &logical.LogInput{Type: "magic"}, | 			Data:                 &logical.LogInput{Type: "magic"}, | ||||||
|  | 			RootNamespace:        true, | ||||||
| 		}, | 		}, | ||||||
| 		"json-response-basic-input": { | 		"json-response-basic-input": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "unable to parse response from audit event: request to response-audit a nil request", | 			ExpectedErrorMessage: "unable to parse request from 'response' audit event: request cannot be nil", | ||||||
| 			Subtype:              ResponseType, | 			Subtype:              ResponseType, | ||||||
| 			RequiredFormat:       JSONFormat, | 			RequiredFormat:       JSONFormat, | ||||||
| 			Data:                 &logical.LogInput{Type: "magic"}, | 			Data:                 &logical.LogInput{Type: "magic"}, | ||||||
| 		}, | 		}, | ||||||
| 		"json-request-basic-input-and-request-no-ns": { | 		"json-request-basic-input-and-request-no-ns": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "unable to parse request from audit event: no namespace", | 			ExpectedErrorMessage: "unable to retrieve namespace from context: no namespace", | ||||||
| 			Subtype:              RequestType, | 			Subtype:              RequestType, | ||||||
| 			RequiredFormat:       JSONFormat, | 			RequiredFormat:       JSONFormat, | ||||||
| 			Data:                 &logical.LogInput{Request: &logical.Request{ID: "123"}}, | 			Data:                 &logical.LogInput{Request: &logical.Request{ID: "123"}}, | ||||||
| 		}, | 		}, | ||||||
| 		"json-response-basic-input-and-request-no-ns": { | 		"json-response-basic-input-and-request-no-ns": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "unable to parse response from audit event: no namespace", | 			ExpectedErrorMessage: "unable to retrieve namespace from context: no namespace", | ||||||
| 			Subtype:              ResponseType, | 			Subtype:              ResponseType, | ||||||
| 			RequiredFormat:       JSONFormat, | 			RequiredFormat:       JSONFormat, | ||||||
| 			Data:                 &logical.LogInput{Request: &logical.Request{ID: "123"}}, | 			Data:                 &logical.LogInput{Request: &logical.Request{ID: "123"}}, | ||||||
| @@ -309,47 +311,52 @@ func TestEntryFormatter_Process(t *testing.T) { | |||||||
| 			IsErrorExpected: false, | 			IsErrorExpected: false, | ||||||
| 			Subtype:         ResponseType, | 			Subtype:         ResponseType, | ||||||
| 			RequiredFormat:  JSONFormat, | 			RequiredFormat:  JSONFormat, | ||||||
| 			Data:            &logical.LogInput{Request: &logical.Request{ID: "123"}}, | 			Data: &logical.LogInput{ | ||||||
| 			RootNamespace:   true, | 				Request:  &logical.Request{ID: "123"}, | ||||||
|  | 				Response: &logical.Response{}, | ||||||
|  | 			}, | ||||||
|  | 			RootNamespace: true, | ||||||
| 		}, | 		}, | ||||||
| 		"jsonx-request-no-data": { | 		"jsonx-request-no-data": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "cannot audit event (request) with no data: invalid internal parameter", | 			ExpectedErrorMessage: "cannot audit a 'request' event with no data: invalid internal parameter", | ||||||
| 			Subtype:              RequestType, | 			Subtype:              RequestType, | ||||||
| 			RequiredFormat:       JSONxFormat, | 			RequiredFormat:       JSONxFormat, | ||||||
| 			Data:                 nil, | 			Data:                 nil, | ||||||
| 		}, | 		}, | ||||||
| 		"jsonx-response-no-data": { | 		"jsonx-response-no-data": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "cannot audit event (response) with no data: invalid internal parameter", | 			ExpectedErrorMessage: "cannot audit a 'response' event with no data: invalid internal parameter", | ||||||
| 			Subtype:              ResponseType, | 			Subtype:              ResponseType, | ||||||
| 			RequiredFormat:       JSONxFormat, | 			RequiredFormat:       JSONxFormat, | ||||||
| 			Data:                 nil, | 			Data:                 nil, | ||||||
| 		}, | 		}, | ||||||
| 		"jsonx-request-basic-input": { | 		"jsonx-request-basic-input": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "unable to parse request from audit event: request to request-audit a nil request", | 			ExpectedErrorMessage: "unable to parse request from 'request' audit event: request cannot be nil", | ||||||
| 			Subtype:              RequestType, | 			Subtype:              RequestType, | ||||||
| 			RequiredFormat:       JSONxFormat, | 			RequiredFormat:       JSONxFormat, | ||||||
| 			Data:                 &logical.LogInput{Type: "magic"}, | 			Data:                 &logical.LogInput{Type: "magic"}, | ||||||
|  | 			RootNamespace:        true, | ||||||
| 		}, | 		}, | ||||||
| 		"jsonx-response-basic-input": { | 		"jsonx-response-basic-input": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "unable to parse response from audit event: request to response-audit a nil request", | 			ExpectedErrorMessage: "unable to parse request from 'response' audit event: request cannot be nil", | ||||||
| 			Subtype:              ResponseType, | 			Subtype:              ResponseType, | ||||||
| 			RequiredFormat:       JSONxFormat, | 			RequiredFormat:       JSONxFormat, | ||||||
| 			Data:                 &logical.LogInput{Type: "magic"}, | 			Data:                 &logical.LogInput{Type: "magic"}, | ||||||
|  | 			RootNamespace:        true, | ||||||
| 		}, | 		}, | ||||||
| 		"jsonx-request-basic-input-and-request-no-ns": { | 		"jsonx-request-basic-input-and-request-no-ns": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "unable to parse request from audit event: no namespace", | 			ExpectedErrorMessage: "unable to retrieve namespace from context: no namespace", | ||||||
| 			Subtype:              RequestType, | 			Subtype:              RequestType, | ||||||
| 			RequiredFormat:       JSONxFormat, | 			RequiredFormat:       JSONxFormat, | ||||||
| 			Data:                 &logical.LogInput{Request: &logical.Request{ID: "123"}}, | 			Data:                 &logical.LogInput{Request: &logical.Request{ID: "123"}}, | ||||||
| 		}, | 		}, | ||||||
| 		"jsonx-response-basic-input-and-request-no-ns": { | 		"jsonx-response-basic-input-and-request-no-ns": { | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "unable to parse response from audit event: no namespace", | 			ExpectedErrorMessage: "unable to retrieve namespace from context: no namespace", | ||||||
| 			Subtype:              ResponseType, | 			Subtype:              ResponseType, | ||||||
| 			RequiredFormat:       JSONxFormat, | 			RequiredFormat:       JSONxFormat, | ||||||
| 			Data:                 &logical.LogInput{Request: &logical.Request{ID: "123"}}, | 			Data:                 &logical.LogInput{Request: &logical.Request{ID: "123"}}, | ||||||
| @@ -365,8 +372,21 @@ func TestEntryFormatter_Process(t *testing.T) { | |||||||
| 			IsErrorExpected: false, | 			IsErrorExpected: false, | ||||||
| 			Subtype:         ResponseType, | 			Subtype:         ResponseType, | ||||||
| 			RequiredFormat:  JSONxFormat, | 			RequiredFormat:  JSONxFormat, | ||||||
| 			Data:            &logical.LogInput{Request: &logical.Request{ID: "123"}}, | 			Data: &logical.LogInput{ | ||||||
| 			RootNamespace:   true, | 				Request:  &logical.Request{ID: "123"}, | ||||||
|  | 				Response: &logical.Response{}, | ||||||
|  | 			}, | ||||||
|  | 			RootNamespace: true, | ||||||
|  | 		}, | ||||||
|  | 		"no-request": { | ||||||
|  | 			IsErrorExpected:      true, | ||||||
|  | 			ExpectedErrorMessage: "unable to parse request from 'response' audit event: request cannot be nil", | ||||||
|  | 			Subtype:              ResponseType, | ||||||
|  | 			RequiredFormat:       JSONxFormat, | ||||||
|  | 			Data: &logical.LogInput{ | ||||||
|  | 				Auth: &logical.Auth{}, | ||||||
|  | 			}, | ||||||
|  | 			RootNamespace: true, | ||||||
| 		}, | 		}, | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| @@ -382,7 +402,7 @@ func TestEntryFormatter_Process(t *testing.T) { | |||||||
| 			cfg, err := newFormatterConfig(&testHeaderFormatter{}, map[string]string{"format": tc.RequiredFormat.String()}) | 			cfg, err := newFormatterConfig(&testHeaderFormatter{}, map[string]string{"format": tc.RequiredFormat.String()}) | ||||||
| 			require.NoError(t, err) | 			require.NoError(t, err) | ||||||
|  |  | ||||||
| 			f, err := newEntryFormatter("juan", cfg, ss, hclog.NewNullLogger()) | 			f, err := newEntryFormatter("juan", cfg, ss, corehelpers.NewTestLogger(t)) | ||||||
| 			require.NoError(t, err) | 			require.NoError(t, err) | ||||||
| 			require.NotNil(t, f) | 			require.NotNil(t, f) | ||||||
|  |  | ||||||
| @@ -475,9 +495,9 @@ func BenchmarkAuditFileSink_Process(b *testing.B) { | |||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
| // TestEntryFormatter_FormatRequest exercises entryFormatter.formatRequest with | // TestEntryFormatter_Process_Request exercises entryFormatter process an event | ||||||
| // varying inputs. | // with varying inputs. | ||||||
| func TestEntryFormatter_FormatRequest(t *testing.T) { | func TestEntryFormatter_Process_Request(t *testing.T) { | ||||||
| 	t.Parallel() | 	t.Parallel() | ||||||
|  |  | ||||||
| 	tests := map[string]struct { | 	tests := map[string]struct { | ||||||
| @@ -490,17 +510,17 @@ func TestEntryFormatter_FormatRequest(t *testing.T) { | |||||||
| 		"nil": { | 		"nil": { | ||||||
| 			Input:                nil, | 			Input:                nil, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "request to request-audit a nil request", | 			ExpectedErrorMessage: "cannot audit a 'request' event with no data: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"basic-input": { | 		"basic-input": { | ||||||
| 			Input:                &logical.LogInput{}, | 			Input:                &logical.LogInput{}, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "request to request-audit a nil request", | 			ExpectedErrorMessage: "unable to parse request from 'request' audit event: request cannot be nil", | ||||||
| 		}, | 		}, | ||||||
| 		"input-and-request-no-ns": { | 		"input-and-request-no-ns": { | ||||||
| 			Input:                &logical.LogInput{Request: &logical.Request{ID: "123"}}, | 			Input:                &logical.LogInput{Request: &logical.Request{ID: "123"}}, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "no namespace", | 			ExpectedErrorMessage: "unable to retrieve namespace from context: no namespace", | ||||||
| 			RootNamespace:        false, | 			RootNamespace:        false, | ||||||
| 		}, | 		}, | ||||||
| 		"input-and-request-with-ns": { | 		"input-and-request-with-ns": { | ||||||
| @@ -536,20 +556,41 @@ func TestEntryFormatter_FormatRequest(t *testing.T) { | |||||||
| 				ctx = context.Background() | 				ctx = context.Background() | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			entry, err := f.formatRequest(ctx, tc.Input, &testTimeProvider{}) | 			auditEvent, err := NewEvent(RequestType) | ||||||
|  | 			auditEvent.setTimeProvider(&testTimeProvider{}) | ||||||
|  | 			require.NoError(t, err) | ||||||
|  | 			auditEvent.Data = tc.Input | ||||||
|  |  | ||||||
|  | 			e := &eventlogger.Event{ | ||||||
|  | 				Type:      event.AuditType.AsEventType(), | ||||||
|  | 				CreatedAt: time.Now(), | ||||||
|  | 				Formatted: make(map[string][]byte), | ||||||
|  | 				Payload:   auditEvent, | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			e2, err := f.Process(ctx, e) | ||||||
| 			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) | ||||||
| 				require.Nil(t, entry) | 				require.Nil(t, e2) | ||||||
| 			case tc.ShouldOmitTime: | 			case tc.ShouldOmitTime: | ||||||
| 				require.NoError(t, err) | 				require.NoError(t, err) | ||||||
| 				require.NotNil(t, entry) | 				require.NotNil(t, e2) | ||||||
|  | 				b, ok := e2.Format(JSONFormat.String()) | ||||||
|  | 				require.True(t, ok) | ||||||
|  | 				var entry *Entry | ||||||
|  | 				err = json.Unmarshal(b, &entry) | ||||||
|  | 				require.NoError(t, err) | ||||||
| 				require.Zero(t, entry.Time) | 				require.Zero(t, entry.Time) | ||||||
| 			default: | 			default: | ||||||
| 				require.NoError(t, err) | 				require.NoError(t, err) | ||||||
| 				require.NotNil(t, entry) | 				require.NotNil(t, e2) | ||||||
|  | 				b, ok := e2.Format(JSONFormat.String()) | ||||||
|  | 				require.True(t, ok) | ||||||
|  | 				var entry *Entry | ||||||
|  | 				err = json.Unmarshal(b, &entry) | ||||||
|  | 				require.NoError(t, err) | ||||||
| 				require.NotZero(t, entry.Time) | 				require.NotZero(t, entry.Time) | ||||||
| 				require.Equal(t, "2024-03-22T10:00:05.00000001Z", entry.Time) | 				require.Equal(t, "2024-03-22T10:00:05.00000001Z", entry.Time) | ||||||
| 			} | 			} | ||||||
| @@ -557,9 +598,9 @@ func TestEntryFormatter_FormatRequest(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // TestEntryFormatter_FormatResponse exercises entryFormatter.formatResponse with | // TestEntryFormatter_Process_ResponseType exercises entryFormatter | ||||||
| // varying inputs. | // with varying inputs also checking if the time can be omitted. | ||||||
| func TestEntryFormatter_FormatResponse(t *testing.T) { | func TestEntryFormatter_Process_ResponseType(t *testing.T) { | ||||||
| 	t.Parallel() | 	t.Parallel() | ||||||
|  |  | ||||||
| 	tests := map[string]struct { | 	tests := map[string]struct { | ||||||
| @@ -572,26 +613,32 @@ func TestEntryFormatter_FormatResponse(t *testing.T) { | |||||||
| 		"nil": { | 		"nil": { | ||||||
| 			Input:                nil, | 			Input:                nil, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "request to response-audit a nil request", | 			ExpectedErrorMessage: "cannot audit a 'response' event with no data: invalid internal parameter", | ||||||
| 		}, | 		}, | ||||||
| 		"basic-input": { | 		"basic-input": { | ||||||
| 			Input:                &logical.LogInput{}, | 			Input:                &logical.LogInput{}, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "request to response-audit a nil request", | 			ExpectedErrorMessage: "unable to parse request from 'response' audit event: request cannot be nil", | ||||||
| 		}, | 		}, | ||||||
| 		"input-and-request-no-ns": { | 		"input-and-request-no-ns": { | ||||||
| 			Input:                &logical.LogInput{Request: &logical.Request{ID: "123"}}, | 			Input:                &logical.LogInput{Request: &logical.Request{ID: "123"}}, | ||||||
| 			IsErrorExpected:      true, | 			IsErrorExpected:      true, | ||||||
| 			ExpectedErrorMessage: "no namespace", | 			ExpectedErrorMessage: "unable to retrieve namespace from context: no namespace", | ||||||
| 			RootNamespace:        false, | 			RootNamespace:        false, | ||||||
| 		}, | 		}, | ||||||
| 		"input-and-request-with-ns": { | 		"input-and-request-with-ns": { | ||||||
| 			Input:           &logical.LogInput{Request: &logical.Request{ID: "123"}}, | 			Input: &logical.LogInput{ | ||||||
|  | 				Request:  &logical.Request{ID: "123"}, | ||||||
|  | 				Response: &logical.Response{}, | ||||||
|  | 			}, | ||||||
| 			IsErrorExpected: false, | 			IsErrorExpected: false, | ||||||
| 			RootNamespace:   true, | 			RootNamespace:   true, | ||||||
| 		}, | 		}, | ||||||
| 		"omit-time": { | 		"omit-time": { | ||||||
| 			Input:           &logical.LogInput{Request: &logical.Request{ID: "123"}}, | 			Input: &logical.LogInput{ | ||||||
|  | 				Request:  &logical.Request{ID: "123"}, | ||||||
|  | 				Response: &logical.Response{}, | ||||||
|  | 			}, | ||||||
| 			ShouldOmitTime:  true, | 			ShouldOmitTime:  true, | ||||||
| 			IsErrorExpected: false, | 			IsErrorExpected: false, | ||||||
| 			RootNamespace:   true, | 			RootNamespace:   true, | ||||||
| @@ -619,20 +666,42 @@ func TestEntryFormatter_FormatResponse(t *testing.T) { | |||||||
| 				ctx = context.Background() | 				ctx = context.Background() | ||||||
| 			} | 			} | ||||||
|  |  | ||||||
| 			entry, err := f.formatResponse(ctx, tc.Input, &testTimeProvider{}) | 			auditEvent, err := NewEvent(ResponseType) | ||||||
|  | 			auditEvent.setTimeProvider(&testTimeProvider{}) | ||||||
|  | 			require.NoError(t, err) | ||||||
|  | 			auditEvent.Data = tc.Input | ||||||
|  |  | ||||||
|  | 			e := &eventlogger.Event{ | ||||||
|  | 				Type:      event.AuditType.AsEventType(), | ||||||
|  | 				CreatedAt: time.Now(), | ||||||
|  | 				Formatted: make(map[string][]byte), | ||||||
|  | 				Payload:   auditEvent, | ||||||
|  | 			} | ||||||
|  |  | ||||||
|  | 			e2, err := f.Process(ctx, e) | ||||||
|  |  | ||||||
| 			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) | ||||||
| 				require.Nil(t, entry) | 				require.Nil(t, e2) | ||||||
| 			case tc.ShouldOmitTime: | 			case tc.ShouldOmitTime: | ||||||
| 				require.NoError(t, err) | 				require.NoError(t, err) | ||||||
| 				require.NotNil(t, entry) | 				require.NotNil(t, e2) | ||||||
|  | 				b, ok := e2.Format(JSONFormat.String()) | ||||||
|  | 				require.True(t, ok) | ||||||
|  | 				var entry *Entry | ||||||
|  | 				err = json.Unmarshal(b, &entry) | ||||||
|  | 				require.NoError(t, err) | ||||||
| 				require.Zero(t, entry.Time) | 				require.Zero(t, entry.Time) | ||||||
| 			default: | 			default: | ||||||
| 				require.NoError(t, err) | 				require.NoError(t, err) | ||||||
| 				require.NotNil(t, entry) | 				require.NotNil(t, e2) | ||||||
|  | 				b, ok := e2.Format(JSONFormat.String()) | ||||||
|  | 				require.True(t, ok) | ||||||
|  | 				var entry *Entry | ||||||
|  | 				err = json.Unmarshal(b, &entry) | ||||||
|  | 				require.NoError(t, err) | ||||||
| 				require.NotZero(t, entry.Time) | 				require.NotZero(t, entry.Time) | ||||||
| 				require.Equal(t, "2024-03-22T10:00:05.00000001Z", entry.Time) | 				require.Equal(t, "2024-03-22T10:00:05.00000001Z", entry.Time) | ||||||
| 			} | 			} | ||||||
| @@ -760,14 +829,14 @@ func TestEntryFormatter_Process_JSON(t *testing.T) { | |||||||
| 			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(RequestEntry) | 		expectedJSON := new(Entry) | ||||||
|  |  | ||||||
| 		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 = &Namespace{ID: "root"} | 		expectedJSON.Request.Namespace = &Namespace{ID: "root"} | ||||||
|  |  | ||||||
| 		actualJSON := new(RequestEntry) | 		actualJSON := new(Entry) | ||||||
| 		if err := jsonutil.DecodeJSON(jsonBytes[len(tc.Prefix):], &actualJSON); err != nil { | 		if err := jsonutil.DecodeJSON(jsonBytes[len(tc.Prefix):], &actualJSON); err != nil { | ||||||
| 			t.Fatalf("bad json: %s", err) | 			t.Fatalf("bad json: %s", err) | ||||||
| 		} | 		} | ||||||
| @@ -933,9 +1002,9 @@ func TestEntryFormatter_Process_JSONx(t *testing.T) { | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| // TestEntryFormatter_FormatResponse_ElideListResponses ensures that we correctly | // TestEntryFormatter_ElideListResponses ensures that we correctly elide data in | ||||||
| // elide data in responses to LIST operations. | // responses to LIST operations. | ||||||
| func TestEntryFormatter_FormatResponse_ElideListResponses(t *testing.T) { | func TestEntryFormatter_ElideListResponses(t *testing.T) { | ||||||
| 	t.Parallel() | 	t.Parallel() | ||||||
|  |  | ||||||
| 	tests := map[string]struct { | 	tests := map[string]struct { | ||||||
| @@ -1006,7 +1075,7 @@ func TestEntryFormatter_FormatResponse_ElideListResponses(t *testing.T) { | |||||||
| 	var formatter *entryFormatter | 	var formatter *entryFormatter | ||||||
| 	var err error | 	var err error | ||||||
|  |  | ||||||
| 	format := func(t *testing.T, config formatterConfig, operation logical.Operation, inputData map[string]any) *ResponseEntry { | 	format := func(t *testing.T, config formatterConfig, operation logical.Operation, inputData map[string]any) *Entry { | ||||||
| 		formatter, err = newEntryFormatter("juan", config, ss, hclog.NewNullLogger()) | 		formatter, err = newEntryFormatter("juan", config, ss, hclog.NewNullLogger()) | ||||||
| 		require.NoError(t, err) | 		require.NoError(t, err) | ||||||
| 		require.NotNil(t, formatter) | 		require.NotNil(t, formatter) | ||||||
| @@ -1016,10 +1085,15 @@ func TestEntryFormatter_FormatResponse_ElideListResponses(t *testing.T) { | |||||||
| 			Response: &logical.Response{Data: inputData}, | 			Response: &logical.Response{Data: inputData}, | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		resp, err := formatter.formatResponse(ctx, in, &testTimeProvider{}) | 		auditEvent, err := NewEvent(ResponseType) | ||||||
| 		require.NoError(t, err) | 		require.NoError(t, err) | ||||||
|  | 		auditEvent.Data = in | ||||||
|  |  | ||||||
| 		return resp | 		entry, err := formatter.createEntry(ctx, auditEvent) | ||||||
|  | 		require.NoError(t, err) | ||||||
|  | 		require.NotNil(t, entry) | ||||||
|  |  | ||||||
|  | 		return entry | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	t.Run("Default case", func(t *testing.T) { | 	t.Run("Default case", func(t *testing.T) { | ||||||
| @@ -1040,7 +1114,8 @@ func TestEntryFormatter_FormatResponse_ElideListResponses(t *testing.T) { | |||||||
| 		require.NoError(t, err) | 		require.NoError(t, err) | ||||||
| 		tc := oneInterestingTestCase | 		tc := oneInterestingTestCase | ||||||
| 		entry := format(t, config, logical.ReadOperation, tc.inputData) | 		entry := format(t, config, logical.ReadOperation, tc.inputData) | ||||||
| 		assert.Equal(t, formatter.hashExpectedValueForComparison(tc.inputData), entry.Response.Data) | 		hashedExpected := formatter.hashExpectedValueForComparison(tc.inputData) | ||||||
|  | 		assert.Equal(t, hashedExpected, entry.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) { | ||||||
| @@ -1113,17 +1188,6 @@ func TestEntryFormatter_Process_NoMutation(t *testing.T) { | |||||||
|  |  | ||||||
| 	// Ensure the pointers are different. | 	// Ensure the pointers are different. | ||||||
| 	require.NotEqual(t, e2, e) | 	require.NotEqual(t, e2, e) | ||||||
|  |  | ||||||
| 	// Do the same for the audit event in the payload. |  | ||||||
| 	a, ok := e.Payload.(*AuditEvent) |  | ||||||
| 	require.True(t, ok) |  | ||||||
| 	require.NotNil(t, a) |  | ||||||
|  |  | ||||||
| 	a2, ok := e2.Payload.(*AuditEvent) |  | ||||||
| 	require.True(t, ok) |  | ||||||
| 	require.NotNil(t, a2) |  | ||||||
|  |  | ||||||
| 	require.NotEqual(t, a2, a) |  | ||||||
| } | } | ||||||
|  |  | ||||||
| // TestEntryFormatter_Process_Panic tries to send data into the entryFormatter | // TestEntryFormatter_Process_Panic tries to send data into the entryFormatter | ||||||
| @@ -1170,6 +1234,7 @@ func TestEntryFormatter_Process_Panic(t *testing.T) { | |||||||
| 			Data: map[string]interface{}{}, | 			Data: map[string]interface{}{}, | ||||||
| 		}, | 		}, | ||||||
| 		Response: &logical.Response{ | 		Response: &logical.Response{ | ||||||
|  | 			Auth: &logical.Auth{}, | ||||||
| 			Data: map[string]any{ | 			Data: map[string]any{ | ||||||
| 				"token_bound_cidrs": []*sockaddr.SockAddrMarshaler{ | 				"token_bound_cidrs": []*sockaddr.SockAddrMarshaler{ | ||||||
| 					{SockAddr: badAddr}, | 					{SockAddr: badAddr}, | ||||||
| @@ -1275,6 +1340,7 @@ func fakeEvent(tb testing.TB, subtype subtype, input *logical.LogInput) *eventlo | |||||||
| 	require.Equal(tb, date, auditEvent.Timestamp) | 	require.Equal(tb, date, auditEvent.Timestamp) | ||||||
|  |  | ||||||
| 	auditEvent.Data = input | 	auditEvent.Data = input | ||||||
|  | 	auditEvent.setTimeProvider(&testTimeProvider{}) | ||||||
|  |  | ||||||
| 	e := &eventlogger.Event{ | 	e := &eventlogger.Event{ | ||||||
| 		Type:      eventlogger.EventType(event.AuditType), | 		Type:      eventlogger.EventType(event.AuditType), | ||||||
|   | |||||||
| @@ -37,6 +37,23 @@ type AuditEvent struct { | |||||||
| 	Subtype   subtype           `json:"subtype"` // the subtype of the audit event. | 	Subtype   subtype           `json:"subtype"` // the subtype of the audit event. | ||||||
| 	Timestamp time.Time         `json:"timestamp"` | 	Timestamp time.Time         `json:"timestamp"` | ||||||
| 	Data      *logical.LogInput `json:"data"` | 	Data      *logical.LogInput `json:"data"` | ||||||
|  | 	prov      timeProvider | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // setTimeProvider can be used to set a specific time provider which is used when | ||||||
|  | // creating an Entry. | ||||||
|  | // NOTE: This is primarily used for testing to supply a known time value. | ||||||
|  | func (a *AuditEvent) setTimeProvider(t timeProvider) { | ||||||
|  | 	a.prov = t | ||||||
|  | } | ||||||
|  |  | ||||||
|  | // timeProvider returns a configured time provider, or the default if not set. | ||||||
|  | func (a *AuditEvent) timeProvider() timeProvider { | ||||||
|  | 	if a.prov == nil { | ||||||
|  | 		return a | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	return a.prov | ||||||
| } | } | ||||||
|  |  | ||||||
| // format defines types of format audit events support. | // format defines types of format audit events support. | ||||||
|   | |||||||
| @@ -11,7 +11,6 @@ import ( | |||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/hashicorp/go-secure-stdlib/strutil" | 	"github.com/hashicorp/go-secure-stdlib/strutil" | ||||||
| 	"github.com/hashicorp/vault/sdk/helper/wrapping" |  | ||||||
| 	"github.com/hashicorp/vault/sdk/logical" | 	"github.com/hashicorp/vault/sdk/logical" | ||||||
| 	"github.com/mitchellh/reflectwalk" | 	"github.com/mitchellh/reflectwalk" | ||||||
| ) | ) | ||||||
| @@ -29,7 +28,7 @@ func hashString(ctx context.Context, salter Salter, data string) (string, error) | |||||||
| // hashAuth uses the Salter to hash the supplied Auth (modifying it). | // hashAuth uses the Salter to hash the supplied Auth (modifying it). | ||||||
| // hmacAccessor is used to indicate whether the accessor should also be HMAC'd | // hmacAccessor is used to indicate whether the accessor should also be HMAC'd | ||||||
| // when present. | // when present. | ||||||
| func hashAuth(ctx context.Context, salter Salter, auth *logical.Auth, hmacAccessor bool) error { | func hashAuth(ctx context.Context, salter Salter, auth *Auth, hmacAccessor bool) error { | ||||||
| 	if auth == nil { | 	if auth == nil { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| @@ -56,7 +55,9 @@ func hashAuth(ctx context.Context, salter Salter, auth *logical.Auth, hmacAccess | |||||||
| // prevents those specific keys from HMAC'd. | // prevents those specific keys from HMAC'd. | ||||||
| // hmacAccessor is used to indicate whether some accessors should also be HMAC'd | // hmacAccessor is used to indicate whether some accessors should also be HMAC'd | ||||||
| // when present. | // when present. | ||||||
| func hashRequest(ctx context.Context, salter Salter, req *logical.Request, hmacAccessor bool, nonHMACDataKeys []string) error { | // nonHMACDataKeys is used when hashing any 'Data' field within the Request which | ||||||
|  | // prevents those specific keys from HMAC'd. | ||||||
|  | func hashRequest(ctx context.Context, salter Salter, req *Request, hmacAccessor bool, nonHMACDataKeys []string) error { | ||||||
| 	if req == nil { | 	if req == nil { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| @@ -68,11 +69,6 @@ func hashRequest(ctx context.Context, salter Salter, req *logical.Request, hmacA | |||||||
|  |  | ||||||
| 	fn := salt.GetIdentifiedHMAC | 	fn := salt.GetIdentifiedHMAC | ||||||
|  |  | ||||||
| 	err = hashAuth(ctx, salter, req.Auth, hmacAccessor) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if req.ClientToken != "" { | 	if req.ClientToken != "" { | ||||||
| 		req.ClientToken = fn(req.ClientToken) | 		req.ClientToken = fn(req.ClientToken) | ||||||
| 	} | 	} | ||||||
| @@ -107,14 +103,12 @@ func hashMap(hashFunc hashCallback, data map[string]interface{}, nonHMACDataKeys | |||||||
| } | } | ||||||
|  |  | ||||||
| // hashResponse uses the Salter to hash the supplied Response (modifying it). | // hashResponse uses the Salter to hash the supplied Response (modifying it). | ||||||
| // nonHMACDataKeys is used when hashing any 'Data' field within the Request which |  | ||||||
| // prevents those specific keys from HMAC'd. |  | ||||||
| // hmacAccessor is used to indicate whether some accessors should also be HMAC'd | // hmacAccessor is used to indicate whether some accessors should also be HMAC'd | ||||||
| // when present. | // when present. | ||||||
| // elideListResponseData indicates whether any 'keys' or 'key_info' data present in | // nonHMACDataKeys is used when hashing any 'Data' field within the Response which | ||||||
| // the Response should be elided (when the request was a LIST operation). | // prevents those specific keys from HMAC'd. | ||||||
| // See: /vault/docs/audit#eliding-list-response-bodies | // See: /vault/docs/audit#eliding-list-response-bodies | ||||||
| func hashResponse(ctx context.Context, salter Salter, resp *logical.Response, hmacAccessor bool, nonHMACDataKeys []string, elideListResponseData bool) error { | func hashResponse(ctx context.Context, salter Salter, resp *Response, hmacAccessor bool, nonHMACDataKeys []string) error { | ||||||
| 	if resp == nil { | 	if resp == nil { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
| @@ -126,23 +120,11 @@ func hashResponse(ctx context.Context, salter Salter, resp *logical.Response, hm | |||||||
|  |  | ||||||
| 	fn := salt.GetIdentifiedHMAC | 	fn := salt.GetIdentifiedHMAC | ||||||
|  |  | ||||||
| 	err = hashAuth(ctx, salter, resp.Auth, hmacAccessor) |  | ||||||
| 	if err != nil { |  | ||||||
| 		return err |  | ||||||
| 	} |  | ||||||
|  |  | ||||||
| 	if resp.Data != nil { | 	if resp.Data != nil { | ||||||
| 		if b, ok := resp.Data[logical.HTTPRawBody].([]byte); ok { | 		if b, ok := resp.Data[logical.HTTPRawBody].([]byte); ok { | ||||||
| 			resp.Data[logical.HTTPRawBody] = string(b) | 			resp.Data[logical.HTTPRawBody] = string(b) | ||||||
| 		} | 		} | ||||||
|  |  | ||||||
| 		// Processing list response data elision takes place at this point in the code for performance reasons: |  | ||||||
| 		// - take advantage of the deep copy of resp.Data that was going to be done anyway for hashing |  | ||||||
| 		// - but elide data before potentially spending time hashing it |  | ||||||
| 		if elideListResponseData { |  | ||||||
| 			doElideListResponseData(resp.Data) |  | ||||||
| 		} |  | ||||||
|  |  | ||||||
| 		err = hashMap(fn, resp.Data, nonHMACDataKeys) | 		err = hashMap(fn, resp.Data, nonHMACDataKeys) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| @@ -160,12 +142,10 @@ func hashResponse(ctx context.Context, salter Salter, resp *logical.Response, hm | |||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
|  |  | ||||||
| // hashWrapInfo returns a hashed copy of the ResponseWrapInfo input. |  | ||||||
|  |  | ||||||
| // hashWrapInfo uses the supplied hashing function to hash ResponseWrapInfo (modifying it). | // hashWrapInfo uses the supplied hashing function to hash ResponseWrapInfo (modifying it). | ||||||
| // hmacAccessor is used to indicate whether some accessors should also be HMAC'd | // hmacAccessor is used to indicate whether some accessors should also be HMAC'd | ||||||
| // when present. | // when present. | ||||||
| func hashWrapInfo(hashFunc hashCallback, wrapInfo *wrapping.ResponseWrapInfo, hmacAccessor bool) error { | func hashWrapInfo(hashFunc hashCallback, wrapInfo *ResponseWrapInfo, hmacAccessor bool) error { | ||||||
| 	if wrapInfo == nil { | 	if wrapInfo == nil { | ||||||
| 		return nil | 		return nil | ||||||
| 	} | 	} | ||||||
|   | |||||||
| @@ -7,12 +7,11 @@ import ( | |||||||
| 	"context" | 	"context" | ||||||
| 	"crypto/sha256" | 	"crypto/sha256" | ||||||
| 	"encoding/json" | 	"encoding/json" | ||||||
| 	"fmt" |  | ||||||
| 	"reflect" | 	"reflect" | ||||||
| 	"testing" | 	"testing" | ||||||
| 	"time" | 	"time" | ||||||
|  |  | ||||||
| 	"github.com/go-test/deep" | 	"github.com/hashicorp/vault/helper/namespace" | ||||||
| 	"github.com/hashicorp/vault/sdk/helper/certutil" | 	"github.com/hashicorp/vault/sdk/helper/certutil" | ||||||
| 	"github.com/hashicorp/vault/sdk/helper/salt" | 	"github.com/hashicorp/vault/sdk/helper/salt" | ||||||
| 	"github.com/hashicorp/vault/sdk/helper/wrapping" | 	"github.com/hashicorp/vault/sdk/helper/wrapping" | ||||||
| @@ -135,34 +134,42 @@ func TestHashString(t *testing.T) { | |||||||
| func TestHashAuth(t *testing.T) { | func TestHashAuth(t *testing.T) { | ||||||
| 	cases := map[string]struct { | 	cases := map[string]struct { | ||||||
| 		Input        *logical.Auth | 		Input        *logical.Auth | ||||||
| 		Output       *logical.Auth | 		Output       *Auth | ||||||
| 		HMACAccessor bool | 		HMACAccessor bool | ||||||
| 	}{ | 	}{ | ||||||
| 		"no-accessor-hmac": { | 		"no-accessor-hmac": { | ||||||
| 			&logical.Auth{ | 			&logical.Auth{ | ||||||
| 				ClientToken: "foo", | 				ClientToken: "foo", | ||||||
| 				Accessor:    "very-accessible", | 				Accessor:    "very-accessible", | ||||||
|  | 				LeaseOptions: logical.LeaseOptions{ | ||||||
|  | 					TTL: 1 * time.Hour, | ||||||
|  | 				}, | ||||||
|  | 				TokenType: logical.TokenTypeService, | ||||||
| 			}, | 			}, | ||||||
| 			&logical.Auth{ | 			&Auth{ | ||||||
| 				ClientToken: "hmac-sha256:08ba357e274f528065766c770a639abf6809b39ccfd37c2a3157c7f51954da0a", | 				ClientToken:   "hmac-sha256:08ba357e274f528065766c770a639abf6809b39ccfd37c2a3157c7f51954da0a", | ||||||
| 				Accessor:    "very-accessible", | 				Accessor:      "very-accessible", | ||||||
|  | 				TokenTTL:      3600, | ||||||
|  | 				TokenType:     "service", | ||||||
|  | 				RemainingUses: 5, | ||||||
| 			}, | 			}, | ||||||
| 			false, | 			false, | ||||||
| 		}, | 		}, | ||||||
| 		"accessor-hmac": { | 		"accessor-hmac": { | ||||||
| 			&logical.Auth{ | 			&logical.Auth{ | ||||||
| 				LeaseOptions: logical.LeaseOptions{ |  | ||||||
| 					TTL: 1 * time.Hour, |  | ||||||
| 				}, |  | ||||||
| 				Accessor:    "very-accessible", | 				Accessor:    "very-accessible", | ||||||
| 				ClientToken: "foo", | 				ClientToken: "foo", | ||||||
| 			}, |  | ||||||
| 			&logical.Auth{ |  | ||||||
| 				LeaseOptions: logical.LeaseOptions{ | 				LeaseOptions: logical.LeaseOptions{ | ||||||
| 					TTL: 1 * time.Hour, | 					TTL: 1 * time.Hour, | ||||||
| 				}, | 				}, | ||||||
| 				Accessor:    "hmac-sha256:5d6d7c8da5b699ace193ea453bbf77082a8aaca42a474436509487d646a7c0af", | 				TokenType: logical.TokenTypeBatch, | ||||||
| 				ClientToken: "hmac-sha256:08ba357e274f528065766c770a639abf6809b39ccfd37c2a3157c7f51954da0a", | 			}, | ||||||
|  | 			&Auth{ | ||||||
|  | 				ClientToken:   "hmac-sha256:08ba357e274f528065766c770a639abf6809b39ccfd37c2a3157c7f51954da0a", | ||||||
|  | 				Accessor:      "hmac-sha256:5d6d7c8da5b699ace193ea453bbf77082a8aaca42a474436509487d646a7c0af", | ||||||
|  | 				TokenTTL:      3600, | ||||||
|  | 				TokenType:     "batch", | ||||||
|  | 				RemainingUses: 5, | ||||||
| 			}, | 			}, | ||||||
| 			true, | 			true, | ||||||
| 		}, | 		}, | ||||||
| @@ -176,9 +183,11 @@ func TestHashAuth(t *testing.T) { | |||||||
| 	require.NoError(t, err) | 	require.NoError(t, err) | ||||||
| 	salter := &TestSalter{} | 	salter := &TestSalter{} | ||||||
| 	for _, tc := range cases { | 	for _, tc := range cases { | ||||||
| 		err := hashAuth(context.Background(), salter, tc.Input, tc.HMACAccessor) | 		auditAuth, err := newAuth(tc.Input, 5) | ||||||
| 		require.NoError(t, err) | 		require.NoError(t, err) | ||||||
| 		require.Equal(t, tc.Output, tc.Input) | 		err = hashAuth(context.Background(), salter, auditAuth, tc.HMACAccessor) | ||||||
|  | 		require.NoError(t, err) | ||||||
|  | 		require.Equal(t, tc.Output, auditAuth) | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -196,7 +205,7 @@ var _ logical.OptMarshaler = &testOptMarshaler{} | |||||||
| func TestHashRequest(t *testing.T) { | func TestHashRequest(t *testing.T) { | ||||||
| 	cases := []struct { | 	cases := []struct { | ||||||
| 		Input           *logical.Request | 		Input           *logical.Request | ||||||
| 		Output          *logical.Request | 		Output          *Request | ||||||
| 		NonHMACDataKeys []string | 		NonHMACDataKeys []string | ||||||
| 		HMACAccessor    bool | 		HMACAccessor    bool | ||||||
| 	}{ | 	}{ | ||||||
| @@ -209,13 +218,17 @@ func TestHashRequest(t *testing.T) { | |||||||
| 					"om":               &testOptMarshaler{S: "bar", I: 1}, | 					"om":               &testOptMarshaler{S: "bar", I: 1}, | ||||||
| 				}, | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			&logical.Request{ | 			&Request{ | ||||||
| 				Data: map[string]interface{}{ | 				Data: map[string]interface{}{ | ||||||
| 					"foo":              "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317", | 					"foo":              "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317", | ||||||
| 					"baz":              "foobar", | 					"baz":              "foobar", | ||||||
| 					"private_key_type": "hmac-sha256:995230dca56fffd310ff591aa404aab52b2abb41703c787cfa829eceb4595bf1", | 					"private_key_type": "hmac-sha256:995230dca56fffd310ff591aa404aab52b2abb41703c787cfa829eceb4595bf1", | ||||||
| 					"om":               json.RawMessage(`{"S":"hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317","I":1}`), | 					"om":               json.RawMessage(`{"S":"hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317","I":1}`), | ||||||
| 				}, | 				}, | ||||||
|  | 				Namespace: &Namespace{ | ||||||
|  | 					ID:   namespace.RootNamespace.ID, | ||||||
|  | 					Path: namespace.RootNamespace.Path, | ||||||
|  | 				}, | ||||||
| 			}, | 			}, | ||||||
| 			[]string{"baz"}, | 			[]string{"baz"}, | ||||||
| 			false, | 			false, | ||||||
| @@ -230,62 +243,61 @@ func TestHashRequest(t *testing.T) { | |||||||
| 	require.NoError(t, err) | 	require.NoError(t, err) | ||||||
| 	salter := &TestSalter{} | 	salter := &TestSalter{} | ||||||
| 	for _, tc := range cases { | 	for _, tc := range cases { | ||||||
| 		input := fmt.Sprintf("%#v", tc.Input) | 		auditReq, err := newRequest(tc.Input, namespace.RootNamespace) | ||||||
| 		err := hashRequest(context.Background(), salter, tc.Input, tc.HMACAccessor, tc.NonHMACDataKeys) | 		require.NoError(t, err) | ||||||
| 		if err != nil { | 		err = hashRequest(context.Background(), salter, auditReq, tc.HMACAccessor, tc.NonHMACDataKeys) | ||||||
| 			t.Fatalf("err: %s\n\n%s", err, input) | 		require.NoError(t, err) | ||||||
| 		} | 		require.Equal(t, tc.Output, auditReq) | ||||||
| 		if diff := deep.Equal(tc.Input, tc.Output); len(diff) > 0 { |  | ||||||
| 			t.Fatalf("bad:\nInput:\n%s\nDiff:\n%#v", input, diff) |  | ||||||
| 		} |  | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestHashResponse(t *testing.T) { | func TestHashResponse(t *testing.T) { | ||||||
| 	now := time.Now() | 	now := time.Now() | ||||||
|  |  | ||||||
| 	cases := []struct { | 	resp := &logical.Response{ | ||||||
| 		Input           *logical.Response | 		Data: map[string]interface{}{ | ||||||
| 		Output          *logical.Response | 			"foo": "bar", | ||||||
| 		NonHMACDataKeys []string | 			"baz": "foobar", | ||||||
| 		HMACAccessor    bool | 			// Responses can contain time values, so test that with a known fixed value. | ||||||
| 	}{ | 			"bar": now, | ||||||
| 		{ | 			"om":  &testOptMarshaler{S: "bar", I: 1}, | ||||||
| 			&logical.Response{ |  | ||||||
| 				Data: map[string]interface{}{ |  | ||||||
| 					"foo": "bar", |  | ||||||
| 					"baz": "foobar", |  | ||||||
| 					// Responses can contain time values, so test that with |  | ||||||
| 					// a known fixed value. |  | ||||||
| 					"bar": now, |  | ||||||
| 					"om":  &testOptMarshaler{S: "bar", I: 1}, |  | ||||||
| 				}, |  | ||||||
| 				WrapInfo: &wrapping.ResponseWrapInfo{ |  | ||||||
| 					TTL:             60, |  | ||||||
| 					Token:           "bar", |  | ||||||
| 					Accessor:        "flimflam", |  | ||||||
| 					CreationTime:    now, |  | ||||||
| 					WrappedAccessor: "bar", |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			&logical.Response{ |  | ||||||
| 				Data: map[string]interface{}{ |  | ||||||
| 					"foo": "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317", |  | ||||||
| 					"baz": "foobar", |  | ||||||
| 					"bar": now.Format(time.RFC3339Nano), |  | ||||||
| 					"om":  json.RawMessage(`{"S":"hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317","I":1}`), |  | ||||||
| 				}, |  | ||||||
| 				WrapInfo: &wrapping.ResponseWrapInfo{ |  | ||||||
| 					TTL:             60, |  | ||||||
| 					Token:           "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317", |  | ||||||
| 					Accessor:        "hmac-sha256:7c9c6fe666d0af73b3ebcfbfabe6885015558213208e6635ba104047b22f6390", |  | ||||||
| 					CreationTime:    now, |  | ||||||
| 					WrappedAccessor: "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317", |  | ||||||
| 				}, |  | ||||||
| 			}, |  | ||||||
| 			[]string{"baz"}, |  | ||||||
| 			true, |  | ||||||
| 		}, | 		}, | ||||||
|  | 		WrapInfo: &wrapping.ResponseWrapInfo{ | ||||||
|  | 			TTL:             1 * time.Minute, | ||||||
|  | 			Token:           "bar", | ||||||
|  | 			Accessor:        "flimflam", | ||||||
|  | 			CreationTime:    now, | ||||||
|  | 			WrappedAccessor: "bar", | ||||||
|  | 		}, | ||||||
|  | 	} | ||||||
|  |  | ||||||
|  | 	req := &logical.Request{MountPoint: "/foo/bar"} | ||||||
|  | 	req.SetMountClass("kv") | ||||||
|  | 	req.SetMountIsExternalPlugin(true) | ||||||
|  | 	req.SetMountRunningVersion("123") | ||||||
|  | 	req.SetMountRunningSha256("256-256!") | ||||||
|  |  | ||||||
|  | 	nonHMACDataKeys := []string{"baz"} | ||||||
|  |  | ||||||
|  | 	expected := &Response{ | ||||||
|  | 		Data: map[string]interface{}{ | ||||||
|  | 			"foo": "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317", | ||||||
|  | 			"baz": "foobar", | ||||||
|  | 			"bar": now.Format(time.RFC3339Nano), | ||||||
|  | 			"om":  json.RawMessage(`{"S":"hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317","I":1}`), | ||||||
|  | 		}, | ||||||
|  | 		WrapInfo: &ResponseWrapInfo{ | ||||||
|  | 			TTL:             60, | ||||||
|  | 			Token:           "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317", | ||||||
|  | 			Accessor:        "hmac-sha256:7c9c6fe666d0af73b3ebcfbfabe6885015558213208e6635ba104047b22f6390", | ||||||
|  | 			CreationTime:    now.UTC().Format(time.RFC3339Nano), | ||||||
|  | 			WrappedAccessor: "hmac-sha256:f9320baf0249169e73850cd6156ded0106e2bb6ad8cab01b7bbbebe6d1065317", | ||||||
|  | 		}, | ||||||
|  | 		MountClass:            "kv", | ||||||
|  | 		MountIsExternalPlugin: true, | ||||||
|  | 		MountPoint:            "/foo/bar", | ||||||
|  | 		MountRunningVersion:   "123", | ||||||
|  | 		MountRunningSha256:    "256-256!", | ||||||
| 	} | 	} | ||||||
|  |  | ||||||
| 	inmemStorage := &logical.InmemStorage{} | 	inmemStorage := &logical.InmemStorage{} | ||||||
| @@ -295,14 +307,11 @@ func TestHashResponse(t *testing.T) { | |||||||
| 	}) | 	}) | ||||||
| 	require.NoError(t, err) | 	require.NoError(t, err) | ||||||
| 	salter := &TestSalter{} | 	salter := &TestSalter{} | ||||||
| 	for _, tc := range cases { | 	auditResp, err := newResponse(resp, req, false) | ||||||
| 		input := fmt.Sprintf("%#v", tc.Input) | 	require.NoError(t, err) | ||||||
| 		err := hashResponse(context.Background(), salter, tc.Input, tc.HMACAccessor, tc.NonHMACDataKeys, false) | 	err = hashResponse(context.Background(), salter, auditResp, true, nonHMACDataKeys) | ||||||
| 		if err != nil { | 	require.NoError(t, err) | ||||||
| 			t.Fatalf("err: %s\n\n%s", err, input) | 	require.Equal(t, expected, auditResp) | ||||||
| 		} |  | ||||||
| 		require.Equal(t, tc.Output, tc.Input) |  | ||||||
| 	} |  | ||||||
| } | } | ||||||
|  |  | ||||||
| func TestHashWalker(t *testing.T) { | func TestHashWalker(t *testing.T) { | ||||||
|   | |||||||
| @@ -7,31 +7,17 @@ import ( | |||||||
| 	"github.com/hashicorp/vault/sdk/logical" | 	"github.com/hashicorp/vault/sdk/logical" | ||||||
| ) | ) | ||||||
|  |  | ||||||
| // NOTE: Any exported changes made to RequestEntry, ResponseEntry or the structs | // Entry represents an audit entry. | ||||||
| // used to compose them, must be reflected in the public facing documentation. | // It could be an entry for a request or response. | ||||||
| // See: /vault/docs/audit (website/content/docs/audit/index.mdx), which at the time | type Entry struct { | ||||||
| // of writing contains JSON examples and JSON schemas intended for use in audit | 	Auth          *Auth     `json:"auth,omitempty"` | ||||||
| // exclusion. | 	Error         string    `json:"error,omitempty"` | ||||||
|  | 	Forwarded     bool      `json:"forwarded,omitempty"` | ||||||
| // RequestEntry is the structure of a request audit log entry. | 	ForwardedFrom string    `json:"forwarded_from,omitempty"` // Populated in Enterprise when a request is forwarded | ||||||
| type RequestEntry struct { | 	Request       *Request  `json:"request,omitempty"` | ||||||
| 	Auth          *Auth    `json:"auth,omitempty"` | 	Response      *Response `json:"response,omitempty"` | ||||||
| 	Error         string   `json:"error,omitempty"` | 	Time          string    `json:"time,omitempty"` | ||||||
| 	ForwardedFrom string   `json:"forwarded_from,omitempty"` // Populated in Enterprise when a request is forwarded | 	Type          string    `json:"type,omitempty"` | ||||||
| 	Request       *Request `json:"request,omitempty"` |  | ||||||
| 	Time          string   `json:"time,omitempty"` |  | ||||||
| 	Type          string   `json:"type,omitempty"` |  | ||||||
| } |  | ||||||
|  |  | ||||||
| // ResponseEntry is the structure of a response audit log entry. |  | ||||||
| type ResponseEntry struct { |  | ||||||
| 	Auth      *Auth     `json:"auth,omitempty"` |  | ||||||
| 	Error     string    `json:"error,omitempty"` |  | ||||||
| 	Forwarded bool      `json:"forwarded,omitempty"` |  | ||||||
| 	Request   *Request  `json:"request,omitempty"` |  | ||||||
| 	Response  *Response `json:"response,omitempty"` |  | ||||||
| 	Time      string    `json:"time,omitempty"` |  | ||||||
| 	Type      string    `json:"type,omitempty"` |  | ||||||
| } | } | ||||||
|  |  | ||||||
| type Request struct { | type Request struct { | ||||||
|   | |||||||
							
								
								
									
										3
									
								
								changelog/27952.txt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								changelog/27952.txt
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,3 @@ | |||||||
|  | ```release-note:improvement | ||||||
|  | audit: Internal implementation changes to the audit subsystem which improve performance. | ||||||
|  | ``` | ||||||
		Reference in New Issue
	
	Block a user
	 Peter Wilson
					Peter Wilson