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

     1  package typesystem
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  
     7  	"github.com/authzed/spicedb/pkg/spiceerrors"
     8  	"github.com/authzed/spicedb/pkg/tuple"
     9  
    10  	core "github.com/authzed/spicedb/pkg/proto/core/v1"
    11  )
    12  
    13  type reachabilityOption int
    14  
    15  const (
    16  	reachabilityFull reachabilityOption = iota
    17  	reachabilityOptimized
    18  )
    19  
    20  func computeReachability(ctx context.Context, ts *TypeSystem, relationName string, option reachabilityOption) (*core.ReachabilityGraph, error) {
    21  	targetRelation, ok := ts.relationMap[relationName]
    22  	if !ok {
    23  		return nil, fmt.Errorf("relation `%s` not found under type `%s` missing when computing reachability", relationName, ts.nsDef.Name)
    24  	}
    25  
    26  	if !ts.HasTypeInformation(relationName) && targetRelation.GetUsersetRewrite() == nil {
    27  		return nil, fmt.Errorf("relation `%s` missing type information when computing reachability for namespace `%s`", relationName, ts.nsDef.Name)
    28  	}
    29  
    30  	graph := &core.ReachabilityGraph{
    31  		EntrypointsBySubjectType:     map[string]*core.ReachabilityEntrypoints{},
    32  		EntrypointsBySubjectRelation: map[string]*core.ReachabilityEntrypoints{},
    33  	}
    34  
    35  	usersetRewrite := targetRelation.GetUsersetRewrite()
    36  	if usersetRewrite != nil {
    37  		return graph, computeRewriteReachability(ctx, graph, usersetRewrite, core.ReachabilityEntrypoint_DIRECT_OPERATION_RESULT, targetRelation, ts, option)
    38  	}
    39  
    40  	// If there is no userRewrite, then we have a relation and its entrypoints will all be
    41  	// relation entrypoints.
    42  	return graph, addSubjectLinks(graph, core.ReachabilityEntrypoint_DIRECT_OPERATION_RESULT, targetRelation, ts)
    43  }
    44  
    45  func computeRewriteReachability(ctx context.Context, graph *core.ReachabilityGraph, rewrite *core.UsersetRewrite, operationResultState core.ReachabilityEntrypoint_EntrypointResultStatus, targetRelation *core.Relation, ts *TypeSystem, option reachabilityOption) error {
    46  	switch rw := rewrite.RewriteOperation.(type) {
    47  	case *core.UsersetRewrite_Union:
    48  		return computeRewriteOpReachability(ctx, rw.Union.Child, operationResultState, graph, targetRelation, ts, option)
    49  
    50  	case *core.UsersetRewrite_Intersection:
    51  		// If optimized mode is set, only return the first child of the intersection.
    52  		if option == reachabilityOptimized {
    53  			return computeRewriteOpReachability(ctx, rw.Intersection.Child[0:1], core.ReachabilityEntrypoint_REACHABLE_CONDITIONAL_RESULT, graph, targetRelation, ts, option)
    54  		}
    55  
    56  		return computeRewriteOpReachability(ctx, rw.Intersection.Child, core.ReachabilityEntrypoint_REACHABLE_CONDITIONAL_RESULT, graph, targetRelation, ts, option)
    57  
    58  	case *core.UsersetRewrite_Exclusion:
    59  		// If optimized mode is set, only return the first child of the exclusion.
    60  		if option == reachabilityOptimized {
    61  			return computeRewriteOpReachability(ctx, rw.Exclusion.Child[0:1], core.ReachabilityEntrypoint_REACHABLE_CONDITIONAL_RESULT, graph, targetRelation, ts, option)
    62  		}
    63  
    64  		return computeRewriteOpReachability(ctx, rw.Exclusion.Child, core.ReachabilityEntrypoint_REACHABLE_CONDITIONAL_RESULT, graph, targetRelation, ts, option)
    65  
    66  	default:
    67  		return fmt.Errorf("unknown kind of userset rewrite in reachability computation: %T", rw)
    68  	}
    69  }
    70  
    71  func computeRewriteOpReachability(ctx context.Context, children []*core.SetOperation_Child, operationResultState core.ReachabilityEntrypoint_EntrypointResultStatus, graph *core.ReachabilityGraph, targetRelation *core.Relation, ts *TypeSystem, option reachabilityOption) error {
    72  	rr := &core.RelationReference{
    73  		Namespace: ts.nsDef.Name,
    74  		Relation:  targetRelation.Name,
    75  	}
    76  
    77  	for _, childOneof := range children {
    78  		switch child := childOneof.ChildType.(type) {
    79  		case *core.SetOperation_Child_XThis:
    80  			return fmt.Errorf("use of _this is unsupported; please rewrite your schema")
    81  
    82  		case *core.SetOperation_Child_ComputedUserset:
    83  			// A computed userset adds an entrypoint indicating that the relation is rewritten.
    84  			err := addSubjectEntrypoint(graph, ts.nsDef.Name, child.ComputedUserset.Relation, &core.ReachabilityEntrypoint{
    85  				Kind:           core.ReachabilityEntrypoint_COMPUTED_USERSET_ENTRYPOINT,
    86  				TargetRelation: rr,
    87  				ResultStatus:   operationResultState,
    88  			})
    89  			if err != nil {
    90  				return err
    91  			}
    92  
    93  		case *core.SetOperation_Child_UsersetRewrite:
    94  			err := computeRewriteReachability(ctx, graph, child.UsersetRewrite, operationResultState, targetRelation, ts, option)
    95  			if err != nil {
    96  				return err
    97  			}
    98  
    99  		case *core.SetOperation_Child_TupleToUserset:
   100  			tuplesetRelation := child.TupleToUserset.Tupleset.Relation
   101  			directRelationTypes, err := ts.AllowedDirectRelationsAndWildcards(tuplesetRelation)
   102  			if err != nil {
   103  				return err
   104  			}
   105  
   106  			computedUsersetRelation := child.TupleToUserset.ComputedUserset.Relation
   107  			for _, allowedRelationType := range directRelationTypes {
   108  				// For each namespace allowed to be found on the right hand side of the
   109  				// tupleset relation, include the *computed userset* relation as an entrypoint.
   110  				//
   111  				// For example, given a schema:
   112  				//
   113  				// ```
   114  				// definition user {}
   115  				//
   116  				// definition parent1 {
   117  				//   relation somerel: user
   118  				// }
   119  				//
   120  				// definition parent2 {
   121  				//   relation somerel: user
   122  				// }
   123  				//
   124  				// definition child {
   125  				//   relation parent: parent1 | parent2
   126  				//   permission someperm = parent->somerel
   127  				// }
   128  				// ```
   129  				//
   130  				// We will add an entrypoint for the arrow itself, keyed to the relation type
   131  				// included from the computed userset.
   132  				//
   133  				// Using the above example, this will add entrypoints for `parent1#somerel`
   134  				// and `parent2#somerel`, which are the subjects reached after resolving the
   135  				// right side of the arrow.
   136  
   137  				// Check if the relation does exist on the allowed type, and only add the entrypoint if present.
   138  				relTypeSystem, err := ts.TypeSystemForNamespace(ctx, allowedRelationType.Namespace)
   139  				if err != nil {
   140  					return err
   141  				}
   142  
   143  				if relTypeSystem.HasRelation(computedUsersetRelation) {
   144  					err := addSubjectEntrypoint(graph, allowedRelationType.Namespace, computedUsersetRelation, &core.ReachabilityEntrypoint{
   145  						Kind:             core.ReachabilityEntrypoint_TUPLESET_TO_USERSET_ENTRYPOINT,
   146  						TargetRelation:   rr,
   147  						ResultStatus:     operationResultState,
   148  						TuplesetRelation: tuplesetRelation,
   149  					})
   150  					if err != nil {
   151  						return err
   152  					}
   153  				}
   154  			}
   155  
   156  		case *core.SetOperation_Child_XNil:
   157  			// nil has no entrypoints.
   158  			return nil
   159  
   160  		default:
   161  			return fmt.Errorf("unknown set operation child `%T` in reachability graph building", child)
   162  		}
   163  	}
   164  
   165  	return nil
   166  }
   167  
   168  func addSubjectEntrypoint(graph *core.ReachabilityGraph, namespaceName string, relationName string, entrypoint *core.ReachabilityEntrypoint) error {
   169  	key := tuple.JoinRelRef(namespaceName, relationName)
   170  	if relationName == "" {
   171  		return spiceerrors.MustBugf("found empty relation name for subject entrypoint")
   172  	}
   173  
   174  	if graph.EntrypointsBySubjectRelation[key] == nil {
   175  		graph.EntrypointsBySubjectRelation[key] = &core.ReachabilityEntrypoints{
   176  			Entrypoints: []*core.ReachabilityEntrypoint{},
   177  			SubjectRelation: &core.RelationReference{
   178  				Namespace: namespaceName,
   179  				Relation:  relationName,
   180  			},
   181  		}
   182  	}
   183  
   184  	graph.EntrypointsBySubjectRelation[key].Entrypoints = append(
   185  		graph.EntrypointsBySubjectRelation[key].Entrypoints,
   186  		entrypoint,
   187  	)
   188  
   189  	return nil
   190  }
   191  
   192  func addSubjectLinks(graph *core.ReachabilityGraph, operationResultState core.ReachabilityEntrypoint_EntrypointResultStatus, relation *core.Relation, ts *TypeSystem) error {
   193  	typeInfo := relation.GetTypeInformation()
   194  	if typeInfo == nil {
   195  		return fmt.Errorf("missing type information for relation %s#%s", ts.nsDef.Name, relation.Name)
   196  	}
   197  
   198  	rr := &core.RelationReference{
   199  		Namespace: ts.nsDef.Name,
   200  		Relation:  relation.Name,
   201  	}
   202  
   203  	allowedDirectRelations := typeInfo.GetAllowedDirectRelations()
   204  	for _, directRelation := range allowedDirectRelations {
   205  		// If the allowed relation is a wildcard, add it as a subject *type* entrypoint, rather than
   206  		// a subject relation.
   207  		if directRelation.GetPublicWildcard() != nil {
   208  			if graph.EntrypointsBySubjectType[directRelation.Namespace] == nil {
   209  				graph.EntrypointsBySubjectType[directRelation.Namespace] = &core.ReachabilityEntrypoints{
   210  					Entrypoints: []*core.ReachabilityEntrypoint{},
   211  					SubjectType: directRelation.Namespace,
   212  				}
   213  			}
   214  
   215  			graph.EntrypointsBySubjectType[directRelation.Namespace].Entrypoints = append(
   216  				graph.EntrypointsBySubjectType[directRelation.Namespace].Entrypoints,
   217  				&core.ReachabilityEntrypoint{
   218  					Kind:           core.ReachabilityEntrypoint_RELATION_ENTRYPOINT,
   219  					TargetRelation: rr,
   220  					ResultStatus:   operationResultState,
   221  				},
   222  			)
   223  			continue
   224  		}
   225  
   226  		err := addSubjectEntrypoint(graph, directRelation.Namespace, directRelation.GetRelation(), &core.ReachabilityEntrypoint{
   227  			Kind:           core.ReachabilityEntrypoint_RELATION_ENTRYPOINT,
   228  			TargetRelation: rr,
   229  			ResultStatus:   operationResultState,
   230  		})
   231  		if err != nil {
   232  			return err
   233  		}
   234  	}
   235  
   236  	return nil
   237  }