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  }