github.com/openfga/openfga@v1.5.4-rc1/internal/graph/graph.go (about)

     1  // Package graph contains code related to evaluation of authorization models through graph traversals.
     2  package graph
     3  
     4  import (
     5  	"context"
     6  	"errors"
     7  	"fmt"
     8  	"strings"
     9  	"sync/atomic"
    10  
    11  	openfgav1 "github.com/openfga/api/proto/openfga/v1"
    12  
    13  	"github.com/openfga/openfga/pkg/tuple"
    14  	"github.com/openfga/openfga/pkg/typesystem"
    15  )
    16  
    17  type ctxKey string
    18  
    19  const (
    20  	resolutionDepthCtxKey ctxKey = "resolution-depth"
    21  )
    22  
    23  var (
    24  	ErrResolutionDepthExceeded = errors.New("resolution depth exceeded")
    25  )
    26  
    27  type findEdgeOption int
    28  
    29  const (
    30  	resolveAllEdges findEdgeOption = iota
    31  	resolveAnyEdge
    32  )
    33  
    34  // ContextWithResolutionDepth attaches the provided graph resolution depth to the parent context.
    35  func ContextWithResolutionDepth(parent context.Context, depth uint32) context.Context {
    36  	return context.WithValue(parent, resolutionDepthCtxKey, depth)
    37  }
    38  
    39  // ResolutionDepthFromContext returns the current graph resolution depth from the provided context (if any).
    40  func ResolutionDepthFromContext(ctx context.Context) (uint32, bool) {
    41  	depth, ok := ctx.Value(resolutionDepthCtxKey).(uint32)
    42  	return depth, ok
    43  }
    44  
    45  type ResolveCheckRequestMetadata struct {
    46  	// Thinking of a Check as a tree of evaluations,
    47  	// Depth is the current level in the tree in the current path that we are exploring.
    48  	// When we jump one level, we decrement 1. If it hits 0, we throw ErrResolutionDepthExceeded.
    49  	Depth uint32
    50  
    51  	// Number of calls to ReadUserTuple + ReadUsersetTuples + Read accumulated so far, before this request is solved.
    52  	DatastoreQueryCount uint32
    53  
    54  	// DispatchCounter is the address to a shared counter that keeps track of how many calls to ResolveCheck we had to do
    55  	// to solve the root/parent problem.
    56  	// The contents of this counter will be written by concurrent goroutines.
    57  	// After the root problem has been solved, this value can be read.
    58  	DispatchCounter *atomic.Uint32
    59  
    60  	// WasThrottled indicates whether the request was throttled
    61  	WasThrottled *atomic.Bool
    62  }
    63  
    64  func NewCheckRequestMetadata(maxDepth uint32) *ResolveCheckRequestMetadata {
    65  	return &ResolveCheckRequestMetadata{
    66  		Depth:               maxDepth,
    67  		DatastoreQueryCount: 0,
    68  		DispatchCounter:     new(atomic.Uint32),
    69  		WasThrottled:        new(atomic.Bool),
    70  	}
    71  }
    72  
    73  type ResolveCheckResponseMetadata struct {
    74  	// Number of calls to ReadUserTuple + ReadUsersetTuples + Read accumulated after this request is solved.
    75  	// Thinking of a Check as a tree of evaluations,
    76  	// If the solution is "allowed=true", one path was found. This is the value in the leaf node of that path, plus the sum of the paths that were
    77  	// evaluated and potentially discarded
    78  	// If the solution is "allowed=false", no paths were found. This is the sum of all the reads in all the paths that had to be evaluated
    79  	DatastoreQueryCount uint32
    80  
    81  	// Indicates if the ResolveCheck subproblem that was evaluated involved
    82  	// a cycle in the evaluation.
    83  	CycleDetected bool
    84  }
    85  
    86  type RelationshipEdgeType int
    87  
    88  const (
    89  	// DirectEdge defines a direct connection between a source object reference
    90  	// and some target user reference.
    91  	DirectEdge RelationshipEdgeType = iota
    92  	// TupleToUsersetEdge defines a connection between a source object reference
    93  	// and some target user reference that is co-dependent upon the lookup of a third object reference.
    94  	TupleToUsersetEdge
    95  	// ComputedUsersetEdge defines a direct connection between a source object reference
    96  	// and some target user reference. The difference with DirectEdge is that DirectEdge will involve
    97  	// a read of tuples and this one will not.
    98  	ComputedUsersetEdge
    99  )
   100  
   101  func (r RelationshipEdgeType) String() string {
   102  	switch r {
   103  	case DirectEdge:
   104  		return "direct"
   105  	case ComputedUsersetEdge:
   106  		return "computed_userset"
   107  	case TupleToUsersetEdge:
   108  		return "ttu"
   109  	default:
   110  		return "undefined"
   111  	}
   112  }
   113  
   114  type EdgeCondition int
   115  
   116  // RelationshipEdge represents a possible relationship between some source object reference
   117  // and a target user reference. The possibility is realized depending on the tuples and on the edge's type.
   118  type RelationshipEdge struct {
   119  	Type RelationshipEdgeType
   120  
   121  	// The edge is directed towards this node, which can be like group:*, or group, or group:member
   122  	TargetReference *openfgav1.RelationReference
   123  
   124  	// If the type is TupleToUsersetEdge, this defines the TTU condition
   125  	TuplesetRelation string
   126  
   127  	TargetReferenceInvolvesIntersectionOrExclusion bool
   128  }
   129  
   130  func (r RelationshipEdge) String() string {
   131  	// TODO also print the condition
   132  	var val string
   133  	if r.TuplesetRelation != "" {
   134  		val = fmt.Sprintf("userset %s, type %s, tupleset %s", r.TargetReference.String(), r.Type.String(), r.TuplesetRelation)
   135  	} else {
   136  		val = fmt.Sprintf("userset %s, type %s", r.TargetReference.String(), r.Type.String())
   137  	}
   138  	return strings.ReplaceAll(val, "  ", " ")
   139  }
   140  
   141  // RelationshipGraph represents a graph of relationships and the connectivity between
   142  // object and relation references within the graph through direct or indirect relationships.
   143  type RelationshipGraph struct {
   144  	typesystem *typesystem.TypeSystem
   145  }
   146  
   147  // New returns a RelationshipGraph from an authorization model. The RelationshipGraph should be used to introspect what kind of relationships between
   148  // object types can exist. To visualize this graph, use https://github.com/jon-whit/openfga-graphviz-gen
   149  func New(typesystem *typesystem.TypeSystem) *RelationshipGraph {
   150  	return &RelationshipGraph{
   151  		typesystem: typesystem,
   152  	}
   153  }
   154  
   155  // GetRelationshipEdges finds all paths from a source to a target and then returns all the edges at distance 0 or 1 of the source in those paths.
   156  func (g *RelationshipGraph) GetRelationshipEdges(target *openfgav1.RelationReference, source *openfgav1.RelationReference) ([]*RelationshipEdge, error) {
   157  	return g.getRelationshipEdges(target, source, map[string]struct{}{}, resolveAllEdges)
   158  }
   159  
   160  // GetPrunedRelationshipEdges finds all paths from a source to a target and then returns all the edges at distance 0 or 1 of the source in those paths.
   161  // If the edges from the source to the target pass through a relationship involving intersection or exclusion (directly or indirectly),
   162  // then GetPrunedRelationshipEdges will just return the first-most edge involved in that rewrite.
   163  //
   164  // Consider the following model:
   165  //
   166  // type user
   167  // type document
   168  //
   169  //	relations
   170  //	  define allowed: [user]
   171  //	  define viewer: [user] and allowed
   172  //
   173  // The pruned relationship edges from the 'user' type to 'document#viewer' returns only the edge from 'user' to 'document#viewer' and with a 'RequiresFurtherEvalCondition'.
   174  // This is because when evaluating relationships involving intersection or exclusion we choose to only evaluate one operand of the rewrite rule, and for each result found
   175  // we call Check on the result to evaluate the sub-condition on the 'and allowed' bit.
   176  func (g *RelationshipGraph) GetPrunedRelationshipEdges(target *openfgav1.RelationReference, source *openfgav1.RelationReference) ([]*RelationshipEdge, error) {
   177  	return g.getRelationshipEdges(target, source, map[string]struct{}{}, resolveAnyEdge)
   178  }
   179  
   180  func (g *RelationshipGraph) getRelationshipEdges(
   181  	target *openfgav1.RelationReference,
   182  	source *openfgav1.RelationReference,
   183  	visited map[string]struct{},
   184  	findEdgeOption findEdgeOption,
   185  ) ([]*RelationshipEdge, error) {
   186  	key := tuple.ToObjectRelationString(target.GetType(), target.GetRelation())
   187  	if _, ok := visited[key]; ok {
   188  		// We've already visited the target so no need to do so again.
   189  		return nil, nil
   190  	}
   191  	visited[key] = struct{}{}
   192  
   193  	relation, err := g.typesystem.GetRelation(target.GetType(), target.GetRelation())
   194  	if err != nil {
   195  		return nil, err
   196  	}
   197  
   198  	return g.getRelationshipEdgesWithTargetRewrite(
   199  		target,
   200  		source,
   201  		relation.GetRewrite(),
   202  		visited,
   203  		findEdgeOption,
   204  	)
   205  }
   206  
   207  // getRelationshipEdgesWithTargetRewrite does a BFS on the graph starting at `target` and trying to reach `source`.
   208  func (g *RelationshipGraph) getRelationshipEdgesWithTargetRewrite(
   209  	target *openfgav1.RelationReference,
   210  	source *openfgav1.RelationReference,
   211  	targetRewrite *openfgav1.Userset,
   212  	visited map[string]struct{},
   213  	findEdgeOption findEdgeOption,
   214  ) ([]*RelationshipEdge, error) {
   215  	switch t := targetRewrite.GetUserset().(type) {
   216  	case *openfgav1.Userset_This: // e.g. define viewer:[user]
   217  		var res []*RelationshipEdge
   218  		directlyRelated, _ := g.typesystem.IsDirectlyRelated(target, source)
   219  		publiclyAssignable, _ := g.typesystem.IsPubliclyAssignable(target, source.GetType())
   220  
   221  		if directlyRelated || publiclyAssignable {
   222  			// if source=user, or define viewer:[user:*]
   223  			res = append(res, &RelationshipEdge{
   224  				Type:            DirectEdge,
   225  				TargetReference: typesystem.DirectRelationReference(target.GetType(), target.GetRelation()),
   226  				TargetReferenceInvolvesIntersectionOrExclusion: false,
   227  			})
   228  		}
   229  
   230  		typeRestrictions, _ := g.typesystem.GetDirectlyRelatedUserTypes(target.GetType(), target.GetRelation())
   231  
   232  		for _, typeRestriction := range typeRestrictions {
   233  			if typeRestriction.GetRelation() != "" { // e.g. define viewer:[team#member]
   234  				// recursively sub-collect any edges for (team#member, source)
   235  				edges, err := g.getRelationshipEdges(typeRestriction, source, visited, findEdgeOption)
   236  				if err != nil {
   237  					return nil, err
   238  				}
   239  
   240  				res = append(res, edges...)
   241  			}
   242  		}
   243  
   244  		return res, nil
   245  	case *openfgav1.Userset_ComputedUserset: // e.g. target = define viewer: writer
   246  
   247  		var edges []*RelationshipEdge
   248  
   249  		// if source=document#writer
   250  		sourceRelMatchesRewritten := target.GetType() == source.GetType() && t.ComputedUserset.GetRelation() == source.GetRelation()
   251  
   252  		if sourceRelMatchesRewritten {
   253  			edges = append(edges, &RelationshipEdge{
   254  				Type:            ComputedUsersetEdge,
   255  				TargetReference: typesystem.DirectRelationReference(target.GetType(), target.GetRelation()),
   256  				TargetReferenceInvolvesIntersectionOrExclusion: false,
   257  			})
   258  		}
   259  
   260  		collected, err := g.getRelationshipEdges(
   261  			typesystem.DirectRelationReference(target.GetType(), t.ComputedUserset.GetRelation()),
   262  			source,
   263  			visited,
   264  			findEdgeOption,
   265  		)
   266  		if err != nil {
   267  			return nil, err
   268  		}
   269  
   270  		edges = append(
   271  			edges,
   272  			collected...,
   273  		)
   274  		return edges, nil
   275  	case *openfgav1.Userset_TupleToUserset: // e.g. type document, define viewer: writer from parent
   276  		tupleset := t.TupleToUserset.GetTupleset().GetRelation()               // parent
   277  		computedUserset := t.TupleToUserset.GetComputedUserset().GetRelation() // writer
   278  
   279  		var res []*RelationshipEdge
   280  		// e.g. type document, define parent:[user, group]
   281  		tuplesetTypeRestrictions, _ := g.typesystem.GetDirectlyRelatedUserTypes(target.GetType(), tupleset)
   282  
   283  		for _, typeRestriction := range tuplesetTypeRestrictions {
   284  			r, err := g.typesystem.GetRelation(typeRestriction.GetType(), computedUserset)
   285  			if err != nil {
   286  				if errors.Is(err, typesystem.ErrRelationUndefined) {
   287  					continue
   288  				}
   289  
   290  				return nil, err
   291  			}
   292  
   293  			if typeRestriction.GetType() == source.GetType() && computedUserset == source.GetRelation() {
   294  				involvesIntersection, err := g.typesystem.RelationInvolvesIntersection(typeRestriction.GetType(), r.GetName())
   295  				if err != nil {
   296  					return nil, err
   297  				}
   298  
   299  				involvesExclusion, err := g.typesystem.RelationInvolvesExclusion(typeRestriction.GetType(), r.GetName())
   300  				if err != nil {
   301  					return nil, err
   302  				}
   303  
   304  				res = append(res, &RelationshipEdge{
   305  					Type:             TupleToUsersetEdge,
   306  					TargetReference:  typesystem.DirectRelationReference(target.GetType(), target.GetRelation()),
   307  					TuplesetRelation: tupleset,
   308  					TargetReferenceInvolvesIntersectionOrExclusion: involvesIntersection || involvesExclusion,
   309  				})
   310  			}
   311  
   312  			subResults, err := g.getRelationshipEdges(
   313  				typesystem.DirectRelationReference(typeRestriction.GetType(), computedUserset),
   314  				source,
   315  				visited,
   316  				findEdgeOption,
   317  			)
   318  			if err != nil {
   319  				return nil, err
   320  			}
   321  
   322  			res = append(res, subResults...)
   323  		}
   324  
   325  		return res, nil
   326  	case *openfgav1.Userset_Union: // e.g. target = define viewer: self or writer
   327  		var res []*RelationshipEdge
   328  		for _, child := range t.Union.GetChild() {
   329  			// we recurse through each child rewrite
   330  			childResults, err := g.getRelationshipEdgesWithTargetRewrite(target, source, child, visited, findEdgeOption)
   331  			if err != nil {
   332  				return nil, err
   333  			}
   334  			res = append(res, childResults...)
   335  		}
   336  		return res, nil
   337  	case *openfgav1.Userset_Intersection:
   338  
   339  		if findEdgeOption == resolveAnyEdge {
   340  			child := t.Intersection.GetChild()[0]
   341  
   342  			childresults, err := g.getRelationshipEdgesWithTargetRewrite(target, source, child, visited, findEdgeOption)
   343  			if err != nil {
   344  				return nil, err
   345  			}
   346  
   347  			for _, childresult := range childresults {
   348  				childresult.TargetReferenceInvolvesIntersectionOrExclusion = true
   349  			}
   350  
   351  			return childresults, nil
   352  		}
   353  
   354  		var edges []*RelationshipEdge
   355  		for _, child := range t.Intersection.GetChild() {
   356  			res, err := g.getRelationshipEdgesWithTargetRewrite(target, source, child, visited, findEdgeOption)
   357  			if err != nil {
   358  				return nil, err
   359  			}
   360  
   361  			edges = append(edges, res...)
   362  		}
   363  
   364  		if len(edges) > 0 {
   365  			edges[0].TargetReferenceInvolvesIntersectionOrExclusion = true
   366  		}
   367  
   368  		return edges, nil
   369  	case *openfgav1.Userset_Difference:
   370  
   371  		if findEdgeOption == resolveAnyEdge {
   372  			// if we have 'a but not b', then we prune 'b' and only resolve 'a' with a
   373  			// condition that requires further evaluation. It's more likely the blacklist
   374  			// on 'but not b' is a larger set than the base set 'a', and so pruning the
   375  			// subtracted set is generally going to be a better choice.
   376  
   377  			child := t.Difference.GetBase()
   378  
   379  			childresults, err := g.getRelationshipEdgesWithTargetRewrite(target, source, child, visited, findEdgeOption)
   380  			if err != nil {
   381  				return nil, err
   382  			}
   383  
   384  			for _, childresult := range childresults {
   385  				childresult.TargetReferenceInvolvesIntersectionOrExclusion = true
   386  			}
   387  
   388  			return childresults, nil
   389  		}
   390  
   391  		var edges []*RelationshipEdge
   392  
   393  		baseRewrite := t.Difference.GetBase()
   394  
   395  		baseEdges, err := g.getRelationshipEdgesWithTargetRewrite(target, source, baseRewrite, visited, findEdgeOption)
   396  		if err != nil {
   397  			return nil, err
   398  		}
   399  
   400  		if len(baseEdges) > 0 {
   401  			baseEdges[0].TargetReferenceInvolvesIntersectionOrExclusion = true
   402  		}
   403  
   404  		edges = append(edges, baseEdges...)
   405  
   406  		subtractRewrite := t.Difference.GetSubtract()
   407  
   408  		subEdges, err := g.getRelationshipEdgesWithTargetRewrite(target, source, subtractRewrite, visited, findEdgeOption)
   409  		if err != nil {
   410  			return nil, err
   411  		}
   412  		edges = append(edges, subEdges...)
   413  
   414  		return edges, nil
   415  	default:
   416  		panic("unexpected userset rewrite encountered")
   417  	}
   418  }
   419  
   420  // NewLayeredCheckResolver constructs a CheckResolver that is composed of various CheckResolver layers.
   421  // Specifically, it constructs a CheckResolver with the following composition:
   422  //
   423  //	CycleDetectionCheckResolver  <-----|
   424  //		CachedCheckResolver              |
   425  //			LocalChecker                   |
   426  //				CycleDetectionCheckResolver -|
   427  //
   428  // The returned CheckResolverCloser should be used to close all resolvers involved in the
   429  // composition after you are done with the CheckResolver.
   430  func NewLayeredCheckResolver(
   431  	localResolverOpts []LocalCheckerOption,
   432  	cacheEnabled bool,
   433  	cachedResolverOpts []CachedCheckResolverOpt,
   434  ) (CheckResolver, CheckResolverCloser) {
   435  	cycleDetectionCheckResolver := NewCycleDetectionCheckResolver()
   436  	localCheckResolver := NewLocalChecker(localResolverOpts...)
   437  
   438  	cycleDetectionCheckResolver.SetDelegate(localCheckResolver)
   439  
   440  	var cachedCheckResolver *CachedCheckResolver
   441  	if cacheEnabled {
   442  		cachedCheckResolver = NewCachedCheckResolver(cachedResolverOpts...)
   443  		cycleDetectionCheckResolver.SetDelegate(cachedCheckResolver)
   444  		cachedCheckResolver.SetDelegate(localCheckResolver)
   445  	}
   446  
   447  	localCheckResolver.SetDelegate(cycleDetectionCheckResolver)
   448  
   449  	return cycleDetectionCheckResolver, func() {
   450  		localCheckResolver.Close()
   451  
   452  		if cachedCheckResolver != nil {
   453  			cachedCheckResolver.Close()
   454  		}
   455  
   456  		cycleDetectionCheckResolver.Close()
   457  	}
   458  }