github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/pkg/caveats/context_hash.go (about)

     1  package caveats
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"net/url"
     7  	"sort"
     8  	"strconv"
     9  
    10  	"golang.org/x/exp/maps"
    11  	"google.golang.org/protobuf/types/known/structpb"
    12  )
    13  
    14  // HasherInterface is an interface for writing context to be hashed.
    15  type HasherInterface interface {
    16  	WriteString(value string)
    17  }
    18  
    19  // StableContextStringForHashing returns a stable string version of the context, for use in hashing.
    20  func StableContextStringForHashing(context *structpb.Struct) string {
    21  	b := bytes.NewBufferString("")
    22  	hc := HashableContext{context}
    23  	hc.AppendToHash(wrappedBuffer{b})
    24  	return b.String()
    25  }
    26  
    27  type wrappedBuffer struct{ *bytes.Buffer }
    28  
    29  func (wb wrappedBuffer) WriteString(value string) {
    30  	wb.Buffer.WriteString(value)
    31  }
    32  
    33  // HashableContext is a wrapper around a context Struct that provides hashing.
    34  type HashableContext struct{ *structpb.Struct }
    35  
    36  func (hc HashableContext) AppendToHash(hasher HasherInterface) {
    37  	// NOTE: the order of keys in the Struct and its resulting JSON output are *unspecified*,
    38  	// as the go runtime randomizes iterator order to ensure that if relied upon, a sort is used.
    39  	// Therefore, we sort the keys here before adding them to the hash.
    40  	if hc.Struct == nil {
    41  		return
    42  	}
    43  
    44  	fields := hc.Struct.Fields
    45  	keys := maps.Keys(fields)
    46  	sort.Strings(keys)
    47  
    48  	for _, key := range keys {
    49  		hasher.WriteString("`")
    50  		hasher.WriteString(key)
    51  		hasher.WriteString("`:")
    52  		hashableStructValue{fields[key]}.AppendToHash(hasher)
    53  		hasher.WriteString(",\n")
    54  	}
    55  }
    56  
    57  type hashableStructValue struct{ *structpb.Value }
    58  
    59  func (hsv hashableStructValue) AppendToHash(hasher HasherInterface) {
    60  	switch t := hsv.Kind.(type) {
    61  	case *structpb.Value_BoolValue:
    62  		hasher.WriteString(strconv.FormatBool(t.BoolValue))
    63  
    64  	case *structpb.Value_ListValue:
    65  		for _, value := range t.ListValue.Values {
    66  			hashableStructValue{value}.AppendToHash(hasher)
    67  			hasher.WriteString(",")
    68  		}
    69  
    70  	case *structpb.Value_NullValue:
    71  		hasher.WriteString("null")
    72  
    73  	case *structpb.Value_NumberValue:
    74  		// AFAICT, this is how Sprintf-style formats float64s
    75  		hasher.WriteString(strconv.FormatFloat(t.NumberValue, 'f', 6, 64))
    76  
    77  	case *structpb.Value_StringValue:
    78  		// NOTE: we escape the string value here to prevent accidental overlap in keys for string
    79  		// values that may themselves contain backticks.
    80  		hasher.WriteString("`" + url.PathEscape(t.StringValue) + "`")
    81  
    82  	case *structpb.Value_StructValue:
    83  		hasher.WriteString("{")
    84  		HashableContext{t.StructValue}.AppendToHash(hasher)
    85  		hasher.WriteString("}")
    86  
    87  	default:
    88  		panic(fmt.Sprintf("unknown struct value type: %T", t))
    89  	}
    90  }