Skip to content

Commit a7608e1

Browse files
committed
fix: deduplicate through alias as well as union, and refactor those functions
1 parent b5db4fc commit a7608e1

File tree

3 files changed

+76
-52
lines changed

3 files changed

+76
-52
lines changed

pkg/query/alias.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,8 @@ func (a *Alias) CheckImpl(ctx *Context, resources []Object, subject ObjectAndRel
3939
return nil, err
4040
}
4141

42-
return func(yield func(Path, error) bool) {
42+
// Create combined sequence with self-edge and rewritten paths
43+
combined := func(yield func(Path, error) bool) {
4344
// Yield the self-edge first
4445
if !yield(selfPath, nil) {
4546
return
@@ -57,7 +58,10 @@ func (a *Alias) CheckImpl(ctx *Context, resources []Object, subject ObjectAndRel
5758
return
5859
}
5960
}
60-
}, nil
61+
}
62+
63+
// Wrap with deduplication to handle duplicate paths after rewriting
64+
return DeduplicatePathSeq(combined), nil
6165
}
6266
}
6367

@@ -67,7 +71,7 @@ func (a *Alias) CheckImpl(ctx *Context, resources []Object, subject ObjectAndRel
6771
return nil, err
6872
}
6973

70-
return func(yield func(Path, error) bool) {
74+
rewritten := func(yield func(Path, error) bool) {
7175
for path, err := range subSeq {
7276
if err != nil {
7377
yield(Path{}, err)
@@ -79,7 +83,10 @@ func (a *Alias) CheckImpl(ctx *Context, resources []Object, subject ObjectAndRel
7983
return
8084
}
8185
}
82-
}, nil
86+
}
87+
88+
// Wrap with deduplication to handle duplicate paths after rewriting
89+
return DeduplicatePathSeq(rewritten), nil
8390
}
8491

8592
func (a *Alias) IterSubjectsImpl(ctx *Context, resource Object) (PathSeq, error) {

pkg/query/path.go

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,12 @@ func (p Path) Key() string {
4646
return fmt.Sprintf("%s#%s@%s", p.Resource.Key(), p.Relation, ObjectAndRelationKey(p.Subject))
4747
}
4848

49+
// EndpointsKey returns a unique string key for this Path based on its resource and subject only,
50+
// excluding the relation. This matches the semantics of EqualsEndpoints.
51+
func (p Path) EndpointsKey() string {
52+
return fmt.Sprintf("%s@%s", p.Resource.Key(), ObjectAndRelationKey(p.Subject))
53+
}
54+
4955
// MergeOr combines the paths, ORing the caveats and expiration and metadata together.
5056
// Returns a new Path with the merged values.
5157
func (p Path) MergeOr(other Path) (Path, error) {
@@ -300,3 +306,39 @@ func CollectAll(seq PathSeq) ([]Path, error) {
300306
}
301307
return out, nil
302308
}
309+
310+
// DeduplicatePathSeq returns a new PathSeq that deduplicates paths based on their
311+
// endpoints (resource and subject, excluding relation). Paths with the same endpoints
312+
// are merged using OR semantics (caveats are OR'd, no caveat wins over caveat).
313+
// This collects all paths first, deduplicates with merging, then yields results.
314+
func DeduplicatePathSeq(seq PathSeq) PathSeq {
315+
return func(yield func(Path, error) bool) {
316+
seen := make(map[string]Path)
317+
for path, err := range seq {
318+
if err != nil {
319+
yield(Path{}, err)
320+
return
321+
}
322+
323+
key := path.EndpointsKey()
324+
if existing, exists := seen[key]; !exists {
325+
seen[key] = path
326+
} else {
327+
// Merge with existing path using OR semantics
328+
merged, err := existing.MergeOr(path)
329+
if err != nil {
330+
yield(Path{}, err)
331+
return
332+
}
333+
seen[key] = merged
334+
}
335+
}
336+
337+
// Yield all deduplicated paths
338+
for _, path := range seen {
339+
if !yield(path, nil) {
340+
return
341+
}
342+
}
343+
}
344+
}

pkg/query/union.go

Lines changed: 23 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -21,62 +21,37 @@ func (u *Union) addSubIterator(subIt Iterator) {
2121
}
2222

2323
func (u *Union) CheckImpl(ctx *Context, resources []Object, subject ObjectAndRelation) (PathSeq, error) {
24-
var out []Path
25-
// Collect paths from all sub-iterators
2624
ctx.TraceStep(u, "processing %d sub-iterators with %d resources", len(u.subIts), len(resources))
2725

28-
for iterIdx, it := range u.subIts {
29-
ctx.TraceStep(u, "processing sub-iterator %d", iterIdx)
26+
// Create a concatenated sequence from all sub-iterators
27+
combinedSeq := func(yield func(Path, error) bool) {
28+
for iterIdx, it := range u.subIts {
29+
ctx.TraceStep(u, "processing sub-iterator %d", iterIdx)
3030

31-
pathSeq, err := ctx.Check(it, resources, subject)
32-
if err != nil {
33-
return nil, err
34-
}
35-
paths, err := CollectAll(pathSeq)
36-
if err != nil {
37-
return nil, err
38-
}
39-
40-
ctx.TraceStep(u, "sub-iterator %d returned %d paths", iterIdx, len(paths))
41-
out = append(out, paths...)
42-
}
43-
44-
ctx.TraceStep(u, "collected %d total paths before deduplication", len(out))
45-
46-
// Deduplicate paths based on resource for CheckImpl
47-
// Since the subject is fixed in CheckImpl, we only need to deduplicate by resource
48-
seen := make(map[string]Path)
49-
for _, path := range out {
50-
// Use resource object (type + id) as key for deduplication, not the full resource with relation
51-
key := path.Resource.Key()
52-
if existing, exists := seen[key]; !exists {
53-
seen[key] = path
54-
} else {
55-
// If we already have a path for this resource,
56-
// merge it with the new one using OR semantics
57-
merged, err := existing.MergeOr(path)
31+
pathSeq, err := ctx.Check(it, resources, subject)
5832
if err != nil {
59-
return nil, err
33+
yield(Path{}, err)
34+
return
6035
}
61-
seen[key] = merged
62-
}
63-
}
6436

65-
// Convert map to slice
66-
deduplicatedSlice := make([]Path, 0, len(seen))
67-
for _, path := range seen {
68-
deduplicatedSlice = append(deduplicatedSlice, path)
69-
}
70-
71-
ctx.TraceStep(u, "deduplicated to %d paths", len(deduplicatedSlice))
72-
73-
return func(yield func(Path, error) bool) {
74-
for _, path := range deduplicatedSlice {
75-
if !yield(path, nil) {
76-
return
37+
pathCount := 0
38+
for path, err := range pathSeq {
39+
if err != nil {
40+
yield(Path{}, err)
41+
return
42+
}
43+
pathCount++
44+
if !yield(path, nil) {
45+
return
46+
}
7747
}
48+
49+
ctx.TraceStep(u, "sub-iterator %d returned %d paths", iterIdx, pathCount)
7850
}
79-
}, nil
51+
}
52+
53+
// Wrap with deduplication
54+
return DeduplicatePathSeq(combinedSeq), nil
8055
}
8156

8257
func (u *Union) IterSubjectsImpl(ctx *Context, resource Object) (PathSeq, error) {

0 commit comments

Comments
 (0)