github.com/authzed/spicedb@v1.32.1-0.20240520085336-ebda56537386/internal/graph/lookupresources.go (about) 1 package graph 2 3 import ( 4 "context" 5 "errors" 6 7 "github.com/authzed/spicedb/internal/dispatch" 8 "github.com/authzed/spicedb/pkg/datastore" 9 core "github.com/authzed/spicedb/pkg/proto/core/v1" 10 v1 "github.com/authzed/spicedb/pkg/proto/dispatch/v1" 11 "github.com/authzed/spicedb/pkg/tuple" 12 ) 13 14 // NewCursoredLookupResources creates and instance of CursoredLookupResources. 15 func NewCursoredLookupResources(c dispatch.Check, r dispatch.ReachableResources, concurrencyLimit uint16) *CursoredLookupResources { 16 return &CursoredLookupResources{c, r, concurrencyLimit} 17 } 18 19 // CursoredLookupResources exposes a method to perform LookupResources requests, and delegates subproblems to the 20 // provided dispatch.Lookup instance. 21 type CursoredLookupResources struct { 22 c dispatch.Check 23 r dispatch.ReachableResources 24 concurrencyLimit uint16 25 } 26 27 // ValidatedLookupResourcesRequest represents a request after it has been validated and parsed for internal 28 // consumption. 29 type ValidatedLookupResourcesRequest struct { 30 *v1.DispatchLookupResourcesRequest 31 Revision datastore.Revision 32 } 33 34 func (cl *CursoredLookupResources) LookupResources( 35 req ValidatedLookupResourcesRequest, 36 parentStream dispatch.LookupResourcesStream, 37 ) error { 38 if req.Subject.ObjectId == tuple.PublicWildcard { 39 return NewErrInvalidArgument(errors.New("cannot perform lookup resources on wildcard")) 40 } 41 42 lookupContext := parentStream.Context() 43 limits := newLimitTracker(req.OptionalLimit) 44 reachableResourcesCursor := req.OptionalCursor 45 46 // Loop until the limit has been exhausted or no additional reachable resources are found (see below) 47 for !limits.hasExhaustedLimit() { 48 errCanceledBecauseNoAdditionalResourcesNeeded := errors.New("canceled because no additional reachable resources are needed") 49 50 // Create a new context for just the reachable resources. This is necessary because we don't want the cancelation 51 // of the reachable resources to cancel the lookup resources. The checking stream manually cancels the reachable 52 // resources context once the expected number of results has been reached. 53 reachableContext, cancelReachable := branchContext(lookupContext) 54 55 // Create a new handling stream that consumes the reachable resources results and publishes them 56 // to the parent stream, as found resources if they are properly checked. 57 checkingStream := newCheckingResourceStream(lookupContext, reachableContext, func() { 58 cancelReachable(errCanceledBecauseNoAdditionalResourcesNeeded) 59 }, req, cl.c, parentStream, limits, cl.concurrencyLimit) 60 61 err := cl.r.DispatchReachableResources(&v1.DispatchReachableResourcesRequest{ 62 ResourceRelation: req.ObjectRelation, 63 SubjectRelation: &core.RelationReference{ 64 Namespace: req.Subject.Namespace, 65 Relation: req.Subject.Relation, 66 }, 67 SubjectIds: []string{req.Subject.ObjectId}, 68 Metadata: req.Metadata, 69 OptionalCursor: reachableResourcesCursor, 70 }, checkingStream) 71 if err != nil { 72 // If the reachable resources was canceled explicitly by the checking stream because the limit has been 73 // reached, then this error can safely be ignored. Otherwise, it must be returned. 74 isAllowedCancelErr := errors.Is(context.Cause(reachableContext), errCanceledBecauseNoAdditionalResourcesNeeded) 75 if !isAllowedCancelErr { 76 return err 77 } 78 } 79 80 reachableCount, newCursor, err := checkingStream.waitForPublishing() 81 if err != nil { 82 return err 83 } 84 85 reachableResourcesCursor = newCursor 86 87 // If no additional reachable results were found or the request was unlimited, then we can stop. 88 if reachableCount == 0 || req.OptionalLimit == 0 { 89 return nil 90 } 91 } 92 93 return nil 94 }