Skip to content

Commit 4c9d349

Browse files
authored
fix: query both subrelation and ellipses on arrows for IterResources (#2879)
1 parent d753f26 commit 4c9d349

File tree

3 files changed

+209
-79
lines changed

3 files changed

+209
-79
lines changed

pkg/query/arrow.go

Lines changed: 183 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
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`
2728
type 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

3436
var _ Iterator = &Arrow{}
3537

3638
func 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

326429
func (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

354458
func (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

pkg/query/build_tree.go

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -314,7 +314,17 @@ func (b *iteratorBuilder) buildArrowIterators(rel *schema.Relation, rightSide st
314314
}
315315
return nil, err
316316
}
317-
arrow := NewArrow(left, right)
317+
// Use NewSchemaArrow only for BaseRelations without subrelations.
318+
// BaseRelations with subrelations (like folder#parent) should use regular arrows
319+
// because they need strict subrelation matching.
320+
var arrow *Arrow
321+
if br.Subrelation() != "" && br.Subrelation() != tuple.Ellipsis {
322+
// Has a specific subrelation: use regular arrow (no ellipsis queries)
323+
arrow = NewArrow(left, right)
324+
} else {
325+
// No subrelation or ellipsis: use schema arrow (with ellipsis queries)
326+
arrow = NewSchemaArrow(left, right)
327+
}
318328
union.addSubIterator(arrow)
319329
}
320330

0 commit comments

Comments
 (0)