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 }