github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/namespace/canonicalization.go (about) 1 package namespace 2 3 import ( 4 "encoding/hex" 5 "hash/fnv" 6 7 "github.com/authzed/spicedb/pkg/spiceerrors" 8 "github.com/authzed/spicedb/pkg/typesystem" 9 10 "github.com/dalzilio/rudd" 11 12 "github.com/authzed/spicedb/pkg/graph" 13 core "github.com/authzed/spicedb/pkg/proto/core/v1" 14 ) 15 16 const computedKeyPrefix = "%" 17 18 // computeCanonicalCacheKeys computes a map from permission name to associated canonicalized 19 // cache key for each non-aliased permission in the given type system's namespace. 20 // 21 // Canonicalization works by taking each permission's userset rewrite expression and transforming 22 // it into a Binary Decision Diagram (BDD) via the `rudd` library. 23 // 24 // Each access of a relation or arrow is assigned a unique integer ID within the *namespace*, 25 // and the operations (+, -, &) are converted into binary operations. 26 // 27 // For example, for the namespace: 28 // 29 // definition somenamespace { 30 // relation first: ... 31 // relation second: ... 32 // relation third: ... 33 // permission someperm = second + (first - third->something) 34 // } 35 // 36 // We begin by assigning a unique integer index to each relation and arrow found for all 37 // expressions in the namespace: 38 // 39 // definition somenamespace { 40 // relation first: ... 41 // ^ index 0 42 // relation second: ... 43 // ^ index 1 44 // relation third: ... 45 // ^ index 2 46 // permission someperm = second + (first - third->something) 47 // ^ 1 ^ 0 ^ index 3 48 // } 49 // 50 // These indexes are then used with the rudd library to build the expression: 51 // 52 // someperm => `bdd.Or(bdd.Ithvar(1), bdd.And(bdd.Ithvar(0), bdd.NIthvar(2)))` 53 // 54 // The `rudd` library automatically handles associativity, and produces a hash representing the 55 // canonical representation of the binary expression. These hashes can then be used for caching, 56 // representing the same *logical* expressions for a permission, even if the relations have 57 // different names. 58 func computeCanonicalCacheKeys(typeSystem *typesystem.ValidatedNamespaceTypeSystem, aliasMap map[string]string) (map[string]string, error) { 59 varMap, err := buildBddVarMap(typeSystem.Namespace().Relation, aliasMap) 60 if err != nil { 61 return nil, err 62 } 63 64 if varMap.Len() == 0 { 65 return map[string]string{}, nil 66 } 67 68 bdd, err := rudd.New(varMap.Len()) 69 if err != nil { 70 return nil, err 71 } 72 73 // For each permission, build a canonicalized cache key based on its expression. 74 cacheKeys := make(map[string]string, len(typeSystem.Namespace().Relation)) 75 for _, rel := range typeSystem.Namespace().Relation { 76 rewrite := rel.GetUsersetRewrite() 77 if rewrite == nil { 78 // If the relation has no rewrite (making it a pure relation), then its canonical 79 // key is simply the relation's name. 80 cacheKeys[rel.Name] = rel.Name 81 continue 82 } 83 84 hasher := fnv.New64a() 85 node, err := convertRewriteToBdd(rel, bdd, rewrite, varMap) 86 if err != nil { 87 return nil, err 88 } 89 90 bdd.Print(hasher, node) 91 cacheKeys[rel.Name] = computedKeyPrefix + hex.EncodeToString(hasher.Sum(nil)) 92 } 93 94 return cacheKeys, nil 95 } 96 97 func convertRewriteToBdd(relation *core.Relation, bdd *rudd.BDD, rewrite *core.UsersetRewrite, varMap bddVarMap) (rudd.Node, error) { 98 switch rw := rewrite.RewriteOperation.(type) { 99 case *core.UsersetRewrite_Union: 100 return convertToBdd(relation, bdd, rw.Union, bdd.Or, func(childIndex int, varIndex int) rudd.Node { 101 return bdd.Ithvar(varIndex) 102 }, varMap) 103 104 case *core.UsersetRewrite_Intersection: 105 return convertToBdd(relation, bdd, rw.Intersection, bdd.And, func(childIndex int, varIndex int) rudd.Node { 106 return bdd.Ithvar(varIndex) 107 }, varMap) 108 109 case *core.UsersetRewrite_Exclusion: 110 return convertToBdd(relation, bdd, rw.Exclusion, bdd.And, func(childIndex int, varIndex int) rudd.Node { 111 if childIndex == 0 { 112 return bdd.Ithvar(varIndex) 113 } 114 return bdd.NIthvar(varIndex) 115 }, varMap) 116 117 default: 118 return nil, spiceerrors.MustBugf("Unknown rewrite kind %v", rw) 119 } 120 } 121 122 type ( 123 combiner func(n ...rudd.Node) rudd.Node 124 builder func(childIndex int, varIndex int) rudd.Node 125 ) 126 127 func convertToBdd(relation *core.Relation, bdd *rudd.BDD, so *core.SetOperation, combiner combiner, builder builder, varMap bddVarMap) (rudd.Node, error) { 128 values := make([]rudd.Node, 0, len(so.Child)) 129 for index, childOneof := range so.Child { 130 switch child := childOneof.ChildType.(type) { 131 case *core.SetOperation_Child_XThis: 132 return nil, spiceerrors.MustBugf("use of _this is disallowed") 133 134 case *core.SetOperation_Child_ComputedUserset: 135 cuIndex, err := varMap.Get(child.ComputedUserset.Relation) 136 if err != nil { 137 return nil, err 138 } 139 140 values = append(values, builder(index, cuIndex)) 141 142 case *core.SetOperation_Child_UsersetRewrite: 143 node, err := convertRewriteToBdd(relation, bdd, child.UsersetRewrite, varMap) 144 if err != nil { 145 return nil, err 146 } 147 148 values = append(values, node) 149 150 case *core.SetOperation_Child_TupleToUserset: 151 arrowIndex, err := varMap.GetArrow(child.TupleToUserset.Tupleset.Relation, child.TupleToUserset.ComputedUserset.Relation) 152 if err != nil { 153 return nil, err 154 } 155 156 values = append(values, builder(index, arrowIndex)) 157 158 case *core.SetOperation_Child_XNil: 159 values = append(values, builder(index, varMap.Nil())) 160 161 default: 162 return nil, spiceerrors.MustBugf("unknown set operation child %T", child) 163 } 164 } 165 return combiner(values...), nil 166 } 167 168 type bddVarMap struct { 169 aliasMap map[string]string 170 varMap map[string]int 171 } 172 173 func (bvm bddVarMap) GetArrow(tuplesetName string, relName string) (int, error) { 174 key := tuplesetName + "->" + relName 175 index, ok := bvm.varMap[key] 176 if !ok { 177 return -1, spiceerrors.MustBugf("missing arrow key %s in varMap", key) 178 } 179 return index, nil 180 } 181 182 func (bvm bddVarMap) Nil() int { 183 return len(bvm.varMap) 184 } 185 186 func (bvm bddVarMap) Get(relName string) (int, error) { 187 if alias, ok := bvm.aliasMap[relName]; ok { 188 return bvm.Get(alias) 189 } 190 191 index, ok := bvm.varMap[relName] 192 if !ok { 193 return -1, spiceerrors.MustBugf("missing key %s in varMap", relName) 194 } 195 return index, nil 196 } 197 198 func (bvm bddVarMap) Len() int { 199 return len(bvm.varMap) + 1 // +1 for `nil` 200 } 201 202 func buildBddVarMap(relations []*core.Relation, aliasMap map[string]string) (bddVarMap, error) { 203 varMap := map[string]int{} 204 for _, rel := range relations { 205 if _, ok := aliasMap[rel.Name]; ok { 206 continue 207 } 208 209 varMap[rel.Name] = len(varMap) 210 211 rewrite := rel.GetUsersetRewrite() 212 if rewrite == nil { 213 continue 214 } 215 216 _, err := graph.WalkRewrite(rewrite, func(childOneof *core.SetOperation_Child) interface{} { 217 switch child := childOneof.ChildType.(type) { 218 case *core.SetOperation_Child_TupleToUserset: 219 key := child.TupleToUserset.Tupleset.Relation + "->" + child.TupleToUserset.ComputedUserset.Relation 220 if _, ok := varMap[key]; !ok { 221 varMap[key] = len(varMap) 222 } 223 } 224 return nil 225 }) 226 if err != nil { 227 return bddVarMap{}, err 228 } 229 } 230 return bddVarMap{ 231 aliasMap: aliasMap, 232 varMap: varMap, 233 }, nil 234 }