64fc18e523
request and response. This makes it far easier to properly check validity elsewhere in Vault because we simply replace the request client token with the inner value.
381 lines
9.5 KiB
Go
381 lines
9.5 KiB
Go
package audit
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/SermoDigital/jose/jws"
|
|
"github.com/hashicorp/vault/logical"
|
|
"github.com/mitchellh/copystructure"
|
|
)
|
|
|
|
type AuditFormatWriter interface {
|
|
WriteRequest(io.Writer, *AuditRequestEntry) error
|
|
WriteResponse(io.Writer, *AuditResponseEntry) error
|
|
}
|
|
|
|
// AuditFormatter implements the Formatter interface, and allows the underlying
|
|
// marshaller to be swapped out
|
|
type AuditFormatter struct {
|
|
AuditFormatWriter
|
|
}
|
|
|
|
func (f *AuditFormatter) FormatRequest(
|
|
w io.Writer,
|
|
config FormatterConfig,
|
|
auth *logical.Auth,
|
|
req *logical.Request,
|
|
err error) error {
|
|
|
|
if w == nil {
|
|
return fmt.Errorf("writer for audit request is nil")
|
|
}
|
|
|
|
if f.AuditFormatWriter == nil {
|
|
return fmt.Errorf("no format writer specified")
|
|
}
|
|
|
|
if !config.Raw {
|
|
// Before we copy the structure we must nil out some data
|
|
// otherwise we will cause reflection to panic and die
|
|
if req.Connection != nil && req.Connection.ConnState != nil {
|
|
origReq := req
|
|
origState := req.Connection.ConnState
|
|
req.Connection.ConnState = nil
|
|
defer func() {
|
|
origReq.Connection.ConnState = origState
|
|
}()
|
|
}
|
|
|
|
// Copy the structures
|
|
cp, err := copystructure.Copy(auth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
auth = cp.(*logical.Auth)
|
|
|
|
cp, err = copystructure.Copy(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req = cp.(*logical.Request)
|
|
|
|
// Hash any sensitive information
|
|
if err := Hash(config.Salt, auth); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Cache and restore accessor in the request
|
|
var clientTokenAccessor string
|
|
if !config.HMACAccessor && req != nil && req.ClientTokenAccessor != "" {
|
|
clientTokenAccessor = req.ClientTokenAccessor
|
|
}
|
|
if err := Hash(config.Salt, req); err != nil {
|
|
return err
|
|
}
|
|
if clientTokenAccessor != "" {
|
|
req.ClientTokenAccessor = clientTokenAccessor
|
|
}
|
|
}
|
|
|
|
// If auth is nil, make an empty one
|
|
if auth == nil {
|
|
auth = new(logical.Auth)
|
|
}
|
|
var errString string
|
|
if err != nil {
|
|
errString = err.Error()
|
|
}
|
|
|
|
reqEntry := &AuditRequestEntry{
|
|
Type: "request",
|
|
Error: errString,
|
|
|
|
Auth: AuditAuth{
|
|
DisplayName: auth.DisplayName,
|
|
Policies: auth.Policies,
|
|
Metadata: auth.Metadata,
|
|
},
|
|
|
|
Request: AuditRequest{
|
|
ID: req.ID,
|
|
ClientToken: req.ClientToken,
|
|
ClientTokenAccessor: req.ClientTokenAccessor,
|
|
Operation: req.Operation,
|
|
Path: req.Path,
|
|
Data: req.Data,
|
|
RemoteAddr: getRemoteAddr(req),
|
|
},
|
|
}
|
|
|
|
if req.WrapInfo != nil {
|
|
reqEntry.Request.WrapTTL = int(req.WrapInfo.TTL / time.Second)
|
|
}
|
|
|
|
if !config.OmitTime {
|
|
reqEntry.Time = time.Now().UTC().Format(time.RFC3339)
|
|
}
|
|
|
|
return f.AuditFormatWriter.WriteRequest(w, reqEntry)
|
|
}
|
|
|
|
func (f *AuditFormatter) FormatResponse(
|
|
w io.Writer,
|
|
config FormatterConfig,
|
|
auth *logical.Auth,
|
|
req *logical.Request,
|
|
resp *logical.Response,
|
|
err error) error {
|
|
|
|
if w == nil {
|
|
return fmt.Errorf("writer for audit request is nil")
|
|
}
|
|
|
|
if f.AuditFormatWriter == nil {
|
|
return fmt.Errorf("no format writer specified")
|
|
}
|
|
|
|
if !config.Raw {
|
|
// Before we copy the structure we must nil out some data
|
|
// otherwise we will cause reflection to panic and die
|
|
if req.Connection != nil && req.Connection.ConnState != nil {
|
|
origReq := req
|
|
origState := req.Connection.ConnState
|
|
req.Connection.ConnState = nil
|
|
defer func() {
|
|
origReq.Connection.ConnState = origState
|
|
}()
|
|
}
|
|
|
|
// Copy the structure
|
|
cp, err := copystructure.Copy(auth)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
auth = cp.(*logical.Auth)
|
|
|
|
cp, err = copystructure.Copy(req)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req = cp.(*logical.Request)
|
|
|
|
cp, err = copystructure.Copy(resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp = cp.(*logical.Response)
|
|
|
|
// Hash any sensitive information
|
|
|
|
// Cache and restore accessor in the auth
|
|
var accessor, wrappedAccessor string
|
|
if !config.HMACAccessor && auth != nil && auth.Accessor != "" {
|
|
accessor = auth.Accessor
|
|
}
|
|
if err := Hash(config.Salt, auth); err != nil {
|
|
return err
|
|
}
|
|
if accessor != "" {
|
|
auth.Accessor = accessor
|
|
}
|
|
|
|
// Cache and restore accessor in the request
|
|
var clientTokenAccessor string
|
|
if !config.HMACAccessor && req != nil && req.ClientTokenAccessor != "" {
|
|
clientTokenAccessor = req.ClientTokenAccessor
|
|
}
|
|
if err := Hash(config.Salt, req); err != nil {
|
|
return err
|
|
}
|
|
if clientTokenAccessor != "" {
|
|
req.ClientTokenAccessor = clientTokenAccessor
|
|
}
|
|
|
|
// Cache and restore accessor in the response
|
|
accessor = ""
|
|
if !config.HMACAccessor && resp != nil && resp.Auth != nil && resp.Auth.Accessor != "" {
|
|
accessor = resp.Auth.Accessor
|
|
}
|
|
if !config.HMACAccessor && resp != nil && resp.WrapInfo != nil && resp.WrapInfo.WrappedAccessor != "" {
|
|
wrappedAccessor = resp.WrapInfo.WrappedAccessor
|
|
}
|
|
if err := Hash(config.Salt, resp); err != nil {
|
|
return err
|
|
}
|
|
if accessor != "" {
|
|
resp.Auth.Accessor = accessor
|
|
}
|
|
if wrappedAccessor != "" {
|
|
resp.WrapInfo.WrappedAccessor = wrappedAccessor
|
|
}
|
|
}
|
|
|
|
// If things are nil, make empty to avoid panics
|
|
if auth == nil {
|
|
auth = new(logical.Auth)
|
|
}
|
|
if resp == nil {
|
|
resp = new(logical.Response)
|
|
}
|
|
var errString string
|
|
if err != nil {
|
|
errString = err.Error()
|
|
}
|
|
|
|
var respAuth *AuditAuth
|
|
if resp.Auth != nil {
|
|
respAuth = &AuditAuth{
|
|
ClientToken: resp.Auth.ClientToken,
|
|
Accessor: resp.Auth.Accessor,
|
|
DisplayName: resp.Auth.DisplayName,
|
|
Policies: resp.Auth.Policies,
|
|
Metadata: resp.Auth.Metadata,
|
|
}
|
|
}
|
|
|
|
var respSecret *AuditSecret
|
|
if resp.Secret != nil {
|
|
respSecret = &AuditSecret{
|
|
LeaseID: resp.Secret.LeaseID,
|
|
}
|
|
}
|
|
|
|
var respWrapInfo *AuditResponseWrapInfo
|
|
if resp.WrapInfo != nil {
|
|
token := resp.WrapInfo.Token
|
|
if jwtToken := parseVaultTokenFromJWT(token); jwtToken != nil {
|
|
token = *jwtToken
|
|
}
|
|
respWrapInfo = &AuditResponseWrapInfo{
|
|
TTL: int(resp.WrapInfo.TTL / time.Second),
|
|
Token: token,
|
|
CreationTime: resp.WrapInfo.CreationTime.Format(time.RFC3339Nano),
|
|
WrappedAccessor: resp.WrapInfo.WrappedAccessor,
|
|
}
|
|
}
|
|
|
|
respEntry := &AuditResponseEntry{
|
|
Type: "response",
|
|
Error: errString,
|
|
|
|
Auth: AuditAuth{
|
|
DisplayName: auth.DisplayName,
|
|
Policies: auth.Policies,
|
|
Metadata: auth.Metadata,
|
|
},
|
|
|
|
Request: AuditRequest{
|
|
ID: req.ID,
|
|
ClientToken: req.ClientToken,
|
|
ClientTokenAccessor: req.ClientTokenAccessor,
|
|
Operation: req.Operation,
|
|
Path: req.Path,
|
|
Data: req.Data,
|
|
RemoteAddr: getRemoteAddr(req),
|
|
},
|
|
|
|
Response: AuditResponse{
|
|
Auth: respAuth,
|
|
Secret: respSecret,
|
|
Data: resp.Data,
|
|
Redirect: resp.Redirect,
|
|
WrapInfo: respWrapInfo,
|
|
},
|
|
}
|
|
|
|
if req.WrapInfo != nil {
|
|
respEntry.Request.WrapTTL = int(req.WrapInfo.TTL / time.Second)
|
|
}
|
|
|
|
if !config.OmitTime {
|
|
respEntry.Time = time.Now().UTC().Format(time.RFC3339)
|
|
}
|
|
|
|
return f.AuditFormatWriter.WriteResponse(w, respEntry)
|
|
}
|
|
|
|
// AuditRequest is the structure of a request audit log entry in Audit.
|
|
type AuditRequestEntry struct {
|
|
Time string `json:"time,omitempty"`
|
|
Type string `json:"type"`
|
|
Auth AuditAuth `json:"auth"`
|
|
Request AuditRequest `json:"request"`
|
|
Error string `json:"error"`
|
|
}
|
|
|
|
// AuditResponseEntry is the structure of a response audit log entry in Audit.
|
|
type AuditResponseEntry struct {
|
|
Time string `json:"time,omitempty"`
|
|
Type string `json:"type"`
|
|
Error string `json:"error"`
|
|
Auth AuditAuth `json:"auth"`
|
|
Request AuditRequest `json:"request"`
|
|
Response AuditResponse `json:"response"`
|
|
}
|
|
|
|
type AuditRequest struct {
|
|
ID string `json:"id"`
|
|
Operation logical.Operation `json:"operation"`
|
|
ClientToken string `json:"client_token"`
|
|
ClientTokenAccessor string `json:"client_token_accessor"`
|
|
Path string `json:"path"`
|
|
Data map[string]interface{} `json:"data"`
|
|
RemoteAddr string `json:"remote_address"`
|
|
WrapTTL int `json:"wrap_ttl"`
|
|
}
|
|
|
|
type AuditResponse struct {
|
|
Auth *AuditAuth `json:"auth,omitempty"`
|
|
Secret *AuditSecret `json:"secret,omitempty"`
|
|
Data map[string]interface{} `json:"data,omitempty"`
|
|
Redirect string `json:"redirect,omitempty"`
|
|
WrapInfo *AuditResponseWrapInfo `json:"wrap_info,omitempty"`
|
|
}
|
|
|
|
type AuditAuth struct {
|
|
ClientToken string `json:"client_token"`
|
|
Accessor string `json:"accessor"`
|
|
DisplayName string `json:"display_name"`
|
|
Policies []string `json:"policies"`
|
|
Metadata map[string]string `json:"metadata"`
|
|
}
|
|
|
|
type AuditSecret struct {
|
|
LeaseID string `json:"lease_id"`
|
|
}
|
|
|
|
type AuditResponseWrapInfo struct {
|
|
TTL int `json:"ttl"`
|
|
Token string `json:"token"`
|
|
CreationTime string `json:"creation_time"`
|
|
WrappedAccessor string `json:"wrapped_accessor,omitempty"`
|
|
}
|
|
|
|
// getRemoteAddr safely gets the remote address avoiding a nil pointer
|
|
func getRemoteAddr(req *logical.Request) string {
|
|
if req != nil && req.Connection != nil {
|
|
return req.Connection.RemoteAddr
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// parseVaultTokenFromJWT returns a string iff the token was a JWT and we could
|
|
// extract the original token ID from inside
|
|
func parseVaultTokenFromJWT(token string) *string {
|
|
if strings.Count(token, ".") != 2 {
|
|
return nil
|
|
}
|
|
|
|
wt, err := jws.ParseJWT([]byte(token))
|
|
if err != nil || wt == nil {
|
|
return nil
|
|
}
|
|
|
|
result, _ := wt.Claims().JWTID()
|
|
|
|
return &result
|
|
}
|