open-vault/audit/hashstructure.go
ncabatoff d2beeefe79
Add support for hashing time.Time within slices (#6767)
Add support for hashing time.Time within slices, which unbreaks auditing of requests returning the request counters.  

Break Hash into struct-specific func like HashAuth, HashRequest. Move all the copying/hashing logic from FormatRequest/FormatResponse into the new Hash* funcs.  HashStructure now modifies in place instead of copying.

Instead of returning an error when trying to hash map keys of type time.Time, ignore them, i.e. pass them through unhashed.

Enable auditing on test clusters by default if the caller didn't specify any audit backends.  If they do, they're responsible for setting it up.
2019-07-02 18:18:40 -04:00

354 lines
9.4 KiB
Go

package audit
import (
"encoding/json"
"errors"
"reflect"
"time"
"github.com/hashicorp/vault/sdk/helper/salt"
"github.com/hashicorp/vault/sdk/helper/strutil"
"github.com/hashicorp/vault/sdk/helper/wrapping"
"github.com/hashicorp/vault/sdk/logical"
"github.com/mitchellh/copystructure"
"github.com/mitchellh/reflectwalk"
)
// HashString hashes the given opaque string and returns it
func HashString(salter *salt.Salt, data string) string {
return salter.GetIdentifiedHMAC(data)
}
// HashAuth returns a hashed copy of the logical.Auth input.
func HashAuth(salter *salt.Salt, in *logical.Auth, HMACAccessor bool) (*logical.Auth, error) {
if in == nil {
return nil, nil
}
fn := salter.GetIdentifiedHMAC
auth := *in
if auth.ClientToken != "" {
auth.ClientToken = fn(auth.ClientToken)
}
if HMACAccessor && auth.Accessor != "" {
auth.Accessor = fn(auth.Accessor)
}
return &auth, nil
}
// HashRequest returns a hashed copy of the logical.Request input.
func HashRequest(salter *salt.Salt, in *logical.Request, HMACAccessor bool, nonHMACDataKeys []string) (*logical.Request, error) {
if in == nil {
return nil, nil
}
fn := salter.GetIdentifiedHMAC
req := *in
if req.Auth != nil {
cp, err := copystructure.Copy(req.Auth)
if err != nil {
return nil, err
}
req.Auth, err = HashAuth(salter, cp.(*logical.Auth), HMACAccessor)
if err != nil {
return nil, err
}
}
if req.ClientToken != "" {
req.ClientToken = fn(req.ClientToken)
}
if HMACAccessor && req.ClientTokenAccessor != "" {
req.ClientTokenAccessor = fn(req.ClientTokenAccessor)
}
data, err := hashMap(fn, req.Data, nonHMACDataKeys)
if err != nil {
return nil, err
}
req.Data = data
return &req, nil
}
func hashMap(fn func(string) string, data map[string]interface{}, nonHMACDataKeys []string) (map[string]interface{}, error) {
if data == nil {
return nil, nil
}
copy, err := copystructure.Copy(data)
if err != nil {
return nil, err
}
newData := copy.(map[string]interface{})
for k, v := range newData {
if o, ok := v.(logical.OptMarshaler); ok {
marshaled, err := o.MarshalJSONWithOptions(&logical.MarshalOptions{
ValueHasher: fn,
})
if err != nil {
return nil, err
}
newData[k] = json.RawMessage(marshaled)
}
}
if err := HashStructure(newData, fn, nonHMACDataKeys); err != nil {
return nil, err
}
return newData, nil
}
// HashResponse returns a hashed copy of the logical.Request input.
func HashResponse(salter *salt.Salt, in *logical.Response, HMACAccessor bool, nonHMACDataKeys []string) (*logical.Response, error) {
if in == nil {
return nil, nil
}
fn := salter.GetIdentifiedHMAC
resp := *in
if resp.Auth != nil {
cp, err := copystructure.Copy(resp.Auth)
if err != nil {
return nil, err
}
resp.Auth, err = HashAuth(salter, cp.(*logical.Auth), HMACAccessor)
if err != nil {
return nil, err
}
}
data, err := hashMap(fn, resp.Data, nonHMACDataKeys)
if err != nil {
return nil, err
}
resp.Data = data
if resp.WrapInfo != nil {
var err error
resp.WrapInfo, err = HashWrapInfo(salter, resp.WrapInfo, HMACAccessor)
if err != nil {
return nil, err
}
}
return &resp, nil
}
// HashWrapInfo returns a hashed copy of the wrapping.ResponseWrapInfo input.
func HashWrapInfo(salter *salt.Salt, in *wrapping.ResponseWrapInfo, HMACAccessor bool) (*wrapping.ResponseWrapInfo, error) {
if in == nil {
return nil, nil
}
fn := salter.GetIdentifiedHMAC
wrapinfo := *in
wrapinfo.Token = fn(wrapinfo.Token)
if HMACAccessor {
wrapinfo.Accessor = fn(wrapinfo.Accessor)
if wrapinfo.WrappedAccessor != "" {
wrapinfo.WrappedAccessor = fn(wrapinfo.WrappedAccessor)
}
}
return &wrapinfo, nil
}
// HashStructure takes an interface and hashes all the values within
// the structure. Only _values_ are hashed: keys of objects are not.
//
// For the HashCallback, see the built-in HashCallbacks below.
func HashStructure(s interface{}, cb HashCallback, ignoredKeys []string) error {
walker := &hashWalker{Callback: cb, IgnoredKeys: ignoredKeys}
return reflectwalk.Walk(s, walker)
}
// HashCallback is the callback called for HashStructure to hash
// a value.
type HashCallback func(string) string
// hashWalker implements interfaces for the reflectwalk package
// (github.com/mitchellh/reflectwalk) that can be used to automatically
// replace primitives with a hashed value.
type hashWalker struct {
// Callback is the function to call with the primitive that is
// to be hashed. If there is an error, walking will be halted
// immediately and the error returned.
Callback HashCallback
// IgnoreKeys are the keys that wont have the HashCallback applied
IgnoredKeys []string
// MapElem appends the key itself (not the reflect.Value) to key.
// The last element in key is the most recently entered map key.
// Since Exit pops the last element of key, only nesting to another
// structure increases the size of this slice.
key []string
lastValue reflect.Value
// Enter appends to loc and exit pops loc. The last element of loc is thus
// the current location.
loc []reflectwalk.Location
// Map and Slice append to cs, Exit pops the last element off cs.
// The last element in cs is the most recently entered map or slice.
cs []reflect.Value
// MapElem and SliceElem append to csKey. The last element in csKey is the
// most recently entered map key or slice index. Since Exit pops the last
// element of csKey, only nesting to another structure increases the size of
// this slice.
csKey []reflect.Value
}
// hashTimeType stores a pre-computed reflect.Type for a time.Time so
// we can quickly compare in hashWalker.Struct. We create an empty/invalid
// time.Time{} so we don't need to incur any additional startup cost vs.
// Now() or Unix().
var hashTimeType = reflect.TypeOf(time.Time{})
func (w *hashWalker) Enter(loc reflectwalk.Location) error {
w.loc = append(w.loc, loc)
return nil
}
func (w *hashWalker) Exit(loc reflectwalk.Location) error {
w.loc = w.loc[:len(w.loc)-1]
switch loc {
case reflectwalk.Map:
w.cs = w.cs[:len(w.cs)-1]
case reflectwalk.MapValue:
w.key = w.key[:len(w.key)-1]
w.csKey = w.csKey[:len(w.csKey)-1]
case reflectwalk.Slice:
w.cs = w.cs[:len(w.cs)-1]
case reflectwalk.SliceElem:
w.csKey = w.csKey[:len(w.csKey)-1]
}
return nil
}
func (w *hashWalker) Map(m reflect.Value) error {
w.cs = append(w.cs, m)
return nil
}
func (w *hashWalker) MapElem(m, k, v reflect.Value) error {
w.csKey = append(w.csKey, k)
w.key = append(w.key, k.String())
w.lastValue = v
return nil
}
func (w *hashWalker) Slice(s reflect.Value) error {
w.cs = append(w.cs, s)
return nil
}
func (w *hashWalker) SliceElem(i int, elem reflect.Value) error {
w.csKey = append(w.csKey, reflect.ValueOf(i))
return nil
}
func (w *hashWalker) Struct(v reflect.Value) error {
// We are looking for time values. If it isn't one, ignore it.
if v.Type() != hashTimeType {
return nil
}
if len(w.loc) < 3 {
// The last element of w.loc is reflectwalk.Struct, by definition.
// If len(w.loc) < 3 that means hashWalker.Walk was given a struct
// value and this is the very first step in the walk, and we don't
// currently support structs as inputs,
return errors.New("structs as direct inputs not supported")
}
// Second to last element of w.loc is location that contains this struct.
switch w.loc[len(w.loc)-2] {
case reflectwalk.MapValue:
// Create a string value of the time. IMPORTANT: this must never change
// across Vault versions or the hash value of equivalent time.Time will
// change.
strVal := v.Interface().(time.Time).Format(time.RFC3339Nano)
// Set the map value to the string instead of the time.Time object
m := w.cs[len(w.cs)-1]
mk := w.csKey[len(w.cs)-1]
m.SetMapIndex(mk, reflect.ValueOf(strVal))
case reflectwalk.SliceElem:
// Create a string value of the time. IMPORTANT: this must never change
// across Vault versions or the hash value of equivalent time.Time will
// change.
strVal := v.Interface().(time.Time).Format(time.RFC3339Nano)
// Set the map value to the string instead of the time.Time object
s := w.cs[len(w.cs)-1]
si := int(w.csKey[len(w.cs)-1].Int())
s.Slice(si, si+1).Index(0).Set(reflect.ValueOf(strVal))
}
// Skip this entry so that we don't walk the struct.
return reflectwalk.SkipEntry
}
func (w *hashWalker) StructField(reflect.StructField, reflect.Value) error {
return nil
}
// Primitive calls Callback to transform strings in-place, except for map keys.
// Strings hiding within interfaces are also transformed.
func (w *hashWalker) Primitive(v reflect.Value) error {
if w.Callback == nil {
return nil
}
// We don't touch map keys
if w.loc[len(w.loc)-1] == reflectwalk.MapKey {
return nil
}
setV := v
// We only care about strings
if v.Kind() == reflect.Interface {
v = v.Elem()
}
if v.Kind() != reflect.String {
return nil
}
// See if the current key is part of the ignored keys
currentKey := w.key[len(w.key)-1]
if strutil.StrListContains(w.IgnoredKeys, currentKey) {
return nil
}
replaceVal := w.Callback(v.String())
resultVal := reflect.ValueOf(replaceVal)
switch w.loc[len(w.loc)-1] {
case reflectwalk.MapValue:
// If we're in a map, then the only way to set a map value is
// to set it directly.
m := w.cs[len(w.cs)-1]
mk := w.csKey[len(w.cs)-1]
m.SetMapIndex(mk, resultVal)
case reflectwalk.SliceElem:
s := w.cs[len(w.cs)-1]
si := int(w.csKey[len(w.cs)-1].Int())
s.Slice(si, si+1).Index(0).Set(resultVal)
default:
// Otherwise, we should be addressable
setV.Set(resultVal)
}
return nil
}