88 "github.com/authzed/spicedb/internal/caveats"
99 core "github.com/authzed/spicedb/pkg/proto/core/v1"
1010 "github.com/authzed/spicedb/pkg/spiceerrors"
11+ "github.com/authzed/spicedb/pkg/tuple"
1112)
1213
1314// arrowDirection specifies which direction to execute the arrow check
@@ -25,20 +26,32 @@ const (
2526//
2627// Ex: `folder->owner` and `left->right`
2728type Arrow struct {
28- id string
29- left Iterator
30- right Iterator
31- direction arrowDirection // execution direction
29+ id string
30+ left Iterator
31+ right Iterator
32+ direction arrowDirection // execution direction
33+ isSchemaArrow bool // true for schema arrows (relation->permission), false for subrelation arrows
3234}
3335
3436var _ Iterator = & Arrow {}
3537
3638func NewArrow (left , right Iterator ) * Arrow {
3739 return & Arrow {
38- id : uuid .NewString (),
39- left : left ,
40- right : right ,
41- direction : leftToRight ,
40+ id : uuid .NewString (),
41+ left : left ,
42+ right : right ,
43+ direction : leftToRight ,
44+ isSchemaArrow : false , // default to subrelation arrow
45+ }
46+ }
47+
48+ func NewSchemaArrow (left , right Iterator ) * Arrow {
49+ return & Arrow {
50+ id : uuid .NewString (),
51+ left : left ,
52+ right : right ,
53+ direction : leftToRight ,
54+ isSchemaArrow : true ,
4255 }
4356}
4457
@@ -92,23 +105,14 @@ func (a *Arrow) checkLeftToRight(ctx *Context, resources []Object, subject Objec
92105 return
93106 }
94107
95- rightPathCount := 0
96- for checkPath , err := range checkit {
97- if err != nil {
98- yield (Path {}, err )
99- return
100- }
101- rightPathCount ++
102-
103- combinedPath := combineArrowPaths (path , checkPath )
104-
105- totalResultPaths ++
106- if ! yield (combinedPath , nil ) {
107- return
108- }
108+ // Process right check results and combine with left path
109+ count , ok := processLeftPathSequence (checkit , path , yield )
110+ totalResultPaths += count
111+ if ! ok {
112+ return
109113 }
110114
111- ctx .TraceStep (a , "right side returned %d paths for subject %s:%s" , rightPathCount , path .Subject .ObjectType , path .Subject .ObjectID )
115+ ctx .TraceStep (a , "right side returned %d paths for subject %s:%s" , count , path .Subject .ObjectType , path .Subject .ObjectID )
112116 }
113117
114118 ctx .TraceStep (a , "left side returned %d paths for resource %s:%s" , leftPathCount , resource .ObjectType , resource .ObjectID )
@@ -156,31 +160,81 @@ func (a *Arrow) checkRightToLeft(ctx *Context, resources []Object, subject Objec
156160 return
157161 }
158162
159- leftPathCount := 0
160- for leftPath , err := range leftSeq {
161- if err != nil {
162- yield (Path {}, err )
163- return
164- }
165- leftPathCount ++
166-
167- combinedPath := combineArrowPaths (leftPath , rightPath )
168-
169- totalResultPaths ++
170- if ! yield (combinedPath , nil ) {
171- return
172- }
163+ // Process left check results and combine with right path
164+ count , ok := processRightPathSequence (leftSeq , rightPath , yield )
165+ totalResultPaths += count
166+ if ! ok {
167+ return
173168 }
174169
175170 ctx .TraceStep (a , "left side returned %d paths for intermediate %s:%s" ,
176- leftPathCount , intermediateAsSubject .ObjectType , intermediateAsSubject .ObjectID )
171+ count , intermediateAsSubject .ObjectType , intermediateAsSubject .ObjectID )
177172 }
178173
179174 ctx .TraceStep (a , "arrow check (right-to-left) completed: %d right paths, %d total result paths" ,
180175 rightPathCount , totalResultPaths )
181176 }, nil
182177}
183178
179+ // processPathSequence is a helper that iterates a path sequence, combines each path with a fixed path,
180+ // and yields the results. It handles error propagation and counting.
181+ //
182+ // Parameters:
183+ // - seq: the sequence to iterate over
184+ // - fixedPath: the path to combine with each iterated path
185+ // - isLeft: if true, fixedPath is the left side in combineArrowPaths; if false, it's the right side
186+ // - yield: the yield function to call for each combined path
187+ //
188+ // Returns (count, ok) where count is the number of paths processed and ok is false if iteration
189+ // should stop (due to error or yield returning false), true otherwise.
190+ func processPathSequence (
191+ seq PathSeq ,
192+ fixedPath Path ,
193+ isLeft bool ,
194+ yield func (Path , error ) bool ,
195+ ) (int , bool ) {
196+ count := 0
197+ for iteratedPath , err := range seq {
198+ if err != nil {
199+ yield (Path {}, err )
200+ return count , false
201+ }
202+
203+ var combinedPath Path
204+ if isLeft {
205+ combinedPath = combineArrowPaths (fixedPath , iteratedPath )
206+ } else {
207+ combinedPath = combineArrowPaths (iteratedPath , fixedPath )
208+ }
209+
210+ count ++
211+ if ! yield (combinedPath , nil ) {
212+ return count , false
213+ }
214+ }
215+ return count , true
216+ }
217+
218+ // processLeftPathSequence processes a sequence where the fixed path is on the left side.
219+ // Returns (count, ok) where count is paths processed and ok indicates whether to continue.
220+ func processLeftPathSequence (
221+ seq PathSeq ,
222+ leftPath Path ,
223+ yield func (Path , error ) bool ,
224+ ) (int , bool ) {
225+ return processPathSequence (seq , leftPath , true , yield )
226+ }
227+
228+ // processRightPathSequence processes a sequence where the fixed path is on the right side.
229+ // Returns (count, ok) where count is paths processed and ok indicates whether to continue.
230+ func processRightPathSequence (
231+ seq PathSeq ,
232+ rightPath Path ,
233+ yield func (Path , error ) bool ,
234+ ) (int , bool ) {
235+ return processPathSequence (seq , rightPath , false , yield )
236+ }
237+
184238// combineArrowPaths combines a left path and right path into a single path for arrow operations.
185239// The combined path uses the resource and relation from the left path, the subject from the right path,
186240// and combines caveats from both sides using AND logic.
@@ -239,23 +293,14 @@ func (a *Arrow) IterSubjectsImpl(ctx *Context, resource Object, filterSubjectTyp
239293 return
240294 }
241295
242- rightPathCount := 0
243- for rightPath , err := range rightSeq {
244- if err != nil {
245- yield (Path {}, err )
246- return
247- }
248- rightPathCount ++
249-
250- combinedPath := combineArrowPaths (leftPath , rightPath )
251-
252- totalResultPaths ++
253- if ! yield (combinedPath , nil ) {
254- return
255- }
296+ // Process right subjects and combine with left path
297+ count , ok := processLeftPathSequence (rightSeq , leftPath , yield )
298+ totalResultPaths += count
299+ if ! ok {
300+ return
256301 }
257302
258- ctx .TraceStep (a , "right side returned %d paths for left subject %s:%s" , rightPathCount , leftSubjectAsResource .ObjectType , leftSubjectAsResource .ObjectID )
303+ ctx .TraceStep (a , "right side returned %d paths for left subject %s:%s" , count , leftSubjectAsResource .ObjectType , leftSubjectAsResource .ObjectID )
259304 }
260305
261306 ctx .TraceStep (a , "arrow IterSubjects completed: %d left paths, %d total result paths" , leftPathCount , totalResultPaths )
@@ -289,34 +334,92 @@ func (a *Arrow) IterResourcesImpl(ctx *Context, subject ObjectAndRelation, filte
289334
290335 rightPathCount ++
291336
292- // For each right resource, get resources from left side
293- // TODO: see if WithEllipses is correct here
294- rightResourceAsSubject := rightPath .Resource .WithEllipses ()
295- ctx .TraceStep (a , "iterating left side for right resource %s:%s" , rightResourceAsSubject .ObjectType , rightResourceAsSubject .ObjectID )
337+ // For each right resource, query the left side.
338+ // For schema arrows (relation->permission), we need to query with BOTH:
339+ // 1. The specific relation from the path
340+ // 2. The same resource with ellipsis (to match stored relationships with ellipsis subjects)
341+ // For subrelation arrows (relation: type#subrelation), we only query with the specific relation.
342+ rightResourceAsSubject := rightPath .ResourceOAR ()
343+ leftPathCount := 0
296344
297- leftSeq , err := ctx .IterResources (a .left , rightResourceAsSubject , filterResourceType )
298- if err != nil {
299- yield (Path {}, err )
300- return
301- }
345+ if a .isSchemaArrow {
346+ // Schema arrow: query with both specific relation and ellipsis
347+ rightResourceWithEllipsis := ObjectAndRelation {
348+ ObjectType : rightResourceAsSubject .ObjectType ,
349+ ObjectID : rightResourceAsSubject .ObjectID ,
350+ Relation : tuple .Ellipsis ,
351+ }
302352
303- leftPathCount := 0
304- for leftPath , err := range leftSeq {
353+ ctx .TraceStep (a , "iterating left side for right resource %s:%s#%s (and ellipsis, schema arrow)" ,
354+ rightResourceAsSubject .ObjectType , rightResourceAsSubject .ObjectID , rightResourceAsSubject .Relation )
355+
356+ // Query with specific relation
357+ leftSeqSpecific , err := ctx .IterResources (a .left , rightResourceAsSubject , filterResourceType )
305358 if err != nil {
306359 yield (Path {}, err )
307360 return
308361 }
309- leftPathCount ++
310362
311- combinedPath := combineArrowPaths (leftPath , rightPath )
363+ // Query with ellipsis
364+ leftSeqEllipsis , err := ctx .IterResources (a .left , rightResourceWithEllipsis , filterResourceType )
365+ if err != nil {
366+ yield (Path {}, err )
367+ return
368+ }
312369
313- totalResultPaths ++
314- if ! yield (combinedPath , nil ) {
370+ // Process both sequences
371+ count1 , ok := processRightPathSequence (leftSeqSpecific , rightPath , yield )
372+ leftPathCount += count1
373+ totalResultPaths += count1
374+ if ! ok {
315375 return
316376 }
317- }
318377
319- ctx .TraceStep (a , "left side returned %d paths for right subject %s:%s" , leftPathCount , rightResourceAsSubject .ObjectType , rightResourceAsSubject .ObjectID )
378+ count2 , ok := processRightPathSequence (leftSeqEllipsis , rightPath , yield )
379+ leftPathCount += count2
380+ totalResultPaths += count2
381+ if ! ok {
382+ return
383+ }
384+
385+ ctx .TraceStep (a , "left side returned %d paths for right subject %s:%s#%s (and ellipsis, schema arrow)" , leftPathCount , rightResourceAsSubject .ObjectType , rightResourceAsSubject .ObjectID , rightResourceAsSubject .Relation )
386+ } else {
387+ // Subrelation arrow: query with the left iterator's expected subrelation
388+ // Get the expected subject types from the left iterator
389+ leftSubjectTypes , err := a .left .SubjectTypes ()
390+ if err != nil {
391+ yield (Path {}, err )
392+ return
393+ }
394+
395+ // Use the expected subrelation from the left iterator
396+ // (for subrelation arrows, there should be exactly one expected type)
397+ if len (leftSubjectTypes ) > 0 && leftSubjectTypes [0 ].Type == rightResourceAsSubject .ObjectType {
398+ expectedSubject := ObjectAndRelation {
399+ ObjectType : rightResourceAsSubject .ObjectType ,
400+ ObjectID : rightResourceAsSubject .ObjectID ,
401+ Relation : leftSubjectTypes [0 ].Subrelation ,
402+ }
403+
404+ ctx .TraceStep (a , "iterating left side for right resource %s:%s#%s (subrelation arrow, using expected subrelation %s)" ,
405+ rightResourceAsSubject .ObjectType , rightResourceAsSubject .ObjectID , rightResourceAsSubject .Relation , expectedSubject .Relation )
406+
407+ leftSeq , err := ctx .IterResources (a .left , expectedSubject , filterResourceType )
408+ if err != nil {
409+ yield (Path {}, err )
410+ return
411+ }
412+
413+ count , ok := processRightPathSequence (leftSeq , rightPath , yield )
414+ leftPathCount += count
415+ totalResultPaths += count
416+ if ! ok {
417+ return
418+ }
419+
420+ ctx .TraceStep (a , "left side returned %d paths for right subject %s:%s#%s (subrelation arrow)" , leftPathCount , expectedSubject .ObjectType , expectedSubject .ObjectID , expectedSubject .Relation )
421+ }
422+ }
320423 }
321424
322425 ctx .TraceStep (a , "arrow IterSubjects completed: %d right paths, %d total result paths" , rightPathCount , totalResultPaths )
@@ -325,10 +428,11 @@ func (a *Arrow) IterResourcesImpl(ctx *Context, subject ObjectAndRelation, filte
325428
326429func (a * Arrow ) Clone () Iterator {
327430 return & Arrow {
328- id : uuid .NewString (),
329- left : a .left .Clone (),
330- right : a .right .Clone (),
331- direction : a .direction , // preserve direction
431+ id : uuid .NewString (),
432+ left : a .left .Clone (),
433+ right : a .right .Clone (),
434+ direction : a .direction , // preserve direction
435+ isSchemaArrow : a .isSchemaArrow , // preserve arrow type
332436 }
333437}
334438
@@ -353,10 +457,11 @@ func (a *Arrow) Subiterators() []Iterator {
353457
354458func (a * Arrow ) ReplaceSubiterators (newSubs []Iterator ) (Iterator , error ) {
355459 return & Arrow {
356- id : uuid .NewString (),
357- left : newSubs [0 ],
358- right : newSubs [1 ],
359- direction : a .direction ,
460+ id : uuid .NewString (),
461+ left : newSubs [0 ],
462+ right : newSubs [1 ],
463+ direction : a .direction ,
464+ isSchemaArrow : a .isSchemaArrow ,
360465 }, nil
361466}
362467
0 commit comments