Skip to content

Commit ea09c1e

Browse files
authored
fix: update IterSubjects for wildcards and Alias iterators for confomance (#2864)
1 parent 10cc7f7 commit ea09c1e

File tree

5 files changed

+277
-128
lines changed

5 files changed

+277
-128
lines changed

internal/datastore/memdb/readonly.go

Lines changed: 4 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -505,15 +505,12 @@ func newSubjectSortedIterator(now time.Time, it memdb.ResultIterator, limit *uin
505505
return nil, err
506506
}
507507

508-
if rt.OptionalExpiration != nil && rt.OptionalExpiration.Before(now) {
509-
continue
510-
}
511-
512508
if skipCaveats && rt.OptionalCaveat != nil {
513509
continue
514510
}
515511

516-
if skipExpiration && rt.OptionalExpiration != nil {
512+
// Only check expiration if skipExpiration is false
513+
if !skipExpiration && rt.OptionalExpiration != nil && rt.OptionalExpiration.Before(now) {
517514
continue
518515
}
519516

@@ -578,11 +575,8 @@ func newMemdbTupleIterator(now time.Time, it memdb.ResultIterator, limit *uint64
578575
continue
579576
}
580577

581-
if skipExpiration && rt.OptionalExpiration != nil {
582-
continue
583-
}
584-
585-
if rt.OptionalExpiration != nil && rt.OptionalExpiration.Before(now) {
578+
// Only check expiration if skipExpiration is false
579+
if !skipExpiration && rt.OptionalExpiration != nil && rt.OptionalExpiration.Before(now) {
586580
continue
587581
}
588582

pkg/query/alias.go

Lines changed: 117 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,9 @@ package query
22

33
import (
44
"github.com/google/uuid"
5+
6+
"github.com/authzed/spicedb/pkg/datastore"
7+
"github.com/authzed/spicedb/pkg/datastore/options"
58
)
69

710
// Alias is an iterator that rewrites the Resource's Relation field of all paths
@@ -24,60 +27,46 @@ func NewAlias(relation string, subIt Iterator) *Alias {
2427
}
2528
}
2629

27-
func (a *Alias) CheckImpl(ctx *Context, resources []Object, subject ObjectAndRelation) (PathSeq, error) {
28-
// First, check for self-edge: if the object with internal relation matches the subject
29-
for _, resource := range resources {
30-
resourceWithAlias := resource.WithRelation(a.relation)
31-
if resourceWithAlias.ObjectID == subject.ObjectID &&
32-
resourceWithAlias.ObjectType == subject.ObjectType &&
33-
resourceWithAlias.Relation == subject.Relation {
34-
// Return the self-edge path first
35-
selfPath := Path{
36-
Resource: GetObject(resourceWithAlias),
37-
Relation: resourceWithAlias.Relation,
38-
Subject: subject,
39-
Metadata: make(map[string]any),
40-
}
41-
42-
// Also get relations from sub-iterator
43-
subSeq, err := ctx.Check(a.subIt, resources, subject)
44-
if err != nil {
45-
return nil, err
46-
}
47-
48-
// Create combined sequence with self-edge and rewritten paths
49-
combined := func(yield func(Path, error) bool) {
50-
// Yield the self-edge first
51-
if !yield(selfPath, nil) {
30+
// maybePrependSelfEdge checks if a self-edge should be added and combines it with the given sequence.
31+
// A self-edge is added when the resource with the alias relation matches the subject.
32+
func (a *Alias) maybePrependSelfEdge(resource Object, subSeq PathSeq, shouldAddSelfEdge bool) PathSeq {
33+
if !shouldAddSelfEdge {
34+
// No self-edge, just rewrite paths from sub-iterator
35+
return func(yield func(Path, error) bool) {
36+
for path, err := range subSeq {
37+
if err != nil {
38+
yield(Path{}, err)
5239
return
5340
}
5441

55-
// Then yield rewritten paths from sub-iterator
56-
for path, err := range subSeq {
57-
if err != nil {
58-
yield(Path{}, err)
59-
return
60-
}
61-
62-
path.Relation = a.relation
63-
if !yield(path, nil) {
64-
return
65-
}
42+
path.Relation = a.relation
43+
if !yield(path, nil) {
44+
return
6645
}
6746
}
68-
69-
// Wrap with deduplication to handle duplicate paths after rewriting
70-
return DeduplicatePathSeq(combined), nil
7147
}
7248
}
7349

74-
// No self-edge detected, just rewrite paths from sub-iterator
75-
subSeq, err := ctx.Check(a.subIt, resources, subject)
76-
if err != nil {
77-
return nil, err
50+
// Create a self-edge path
51+
selfPath := Path{
52+
Resource: resource,
53+
Relation: a.relation,
54+
Subject: ObjectAndRelation{
55+
ObjectType: resource.ObjectType,
56+
ObjectID: resource.ObjectID,
57+
Relation: a.relation,
58+
},
59+
Metadata: make(map[string]any),
7860
}
7961

80-
rewritten := func(yield func(Path, error) bool) {
62+
// Combine self-edge with paths from sub-iterator
63+
combined := func(yield func(Path, error) bool) {
64+
// Yield the self-edge first
65+
if !yield(selfPath, nil) {
66+
return
67+
}
68+
69+
// Then yield rewritten paths from sub-iterator
8170
for path, err := range subSeq {
8271
if err != nil {
8372
yield(Path{}, err)
@@ -91,84 +80,111 @@ func (a *Alias) CheckImpl(ctx *Context, resources []Object, subject ObjectAndRel
9180
}
9281
}
9382

94-
// Wrap with deduplication to handle duplicate paths after rewriting
95-
return DeduplicatePathSeq(rewritten), nil
83+
// Wrap with deduplication to handle duplicate paths
84+
return DeduplicatePathSeq(combined)
9685
}
9786

98-
func (a *Alias) IterSubjectsImpl(ctx *Context, resource Object, filterSubjectType ObjectType) (PathSeq, error) {
99-
subSeq, err := ctx.IterSubjects(a.subIt, resource, filterSubjectType)
87+
func (a *Alias) CheckImpl(ctx *Context, resources []Object, subject ObjectAndRelation) (PathSeq, error) {
88+
// Get relations from sub-iterator
89+
subSeq, err := ctx.Check(a.subIt, resources, subject)
10090
if err != nil {
10191
return nil, err
10292
}
10393

104-
return func(yield func(Path, error) bool) {
105-
for path, err := range subSeq {
106-
if err != nil {
107-
yield(Path{}, err)
108-
return
109-
}
110-
111-
path.Relation = a.relation
112-
if !yield(path, nil) {
113-
return
114-
}
94+
// Check for self-edge: if any resource with the alias relation matches the subject
95+
for _, resource := range resources {
96+
resourceWithAlias := resource.WithRelation(a.relation)
97+
if resourceWithAlias.ObjectID == subject.ObjectID &&
98+
resourceWithAlias.ObjectType == subject.ObjectType &&
99+
resourceWithAlias.Relation == subject.Relation {
100+
return a.maybePrependSelfEdge(GetObject(resourceWithAlias), subSeq, true), nil
115101
}
116-
}, nil
102+
}
103+
104+
// No self-edge detected, just rewrite paths from sub-iterator
105+
return DeduplicatePathSeq(a.maybePrependSelfEdge(Object{}, subSeq, false)), nil
117106
}
118107

119-
func (a *Alias) IterResourcesImpl(ctx *Context, subject ObjectAndRelation, filterResourceType ObjectType) (PathSeq, error) {
120-
subSeq, err := ctx.IterResources(a.subIt, subject, filterResourceType)
108+
func (a *Alias) IterSubjectsImpl(ctx *Context, resource Object, filterSubjectType ObjectType) (PathSeq, error) {
109+
subSeq, err := ctx.IterSubjects(a.subIt, resource, filterSubjectType)
121110
if err != nil {
122111
return nil, err
123112
}
124113

125-
// If the relation on the alias iterator is the same
126-
// as the subject relation on the subject, we have
127-
// a self edge and want to yield it accordingly.
128-
if a.relation == subject.Relation {
129-
selfPath := Path{
130-
Resource: GetObject(subject),
131-
Relation: a.relation,
132-
Subject: subject,
133-
}
114+
// Check if we should add a self-edge based on identity semantics.
115+
// The dispatcher Check includes an identity check (see filterForFoundMemberResource
116+
// in internal/graph/check.go): if the resource (with relation) matches the subject
117+
// exactly, it returns MEMBER. This only applies if the resource actually appears
118+
// as a subject in the data and the filter allows it.
119+
shouldAddSelfEdge := a.shouldIncludeSelfEdge(ctx, resource, filterSubjectType)
134120

135-
combined := func(yield func(Path, error) bool) {
136-
// Yield the self path first
137-
if !yield(selfPath, nil) {
138-
return
139-
}
121+
return a.maybePrependSelfEdge(resource, subSeq, shouldAddSelfEdge), nil
122+
}
140123

141-
for subPath, err := range subSeq {
142-
if err != nil {
143-
yield(Path{}, err)
144-
return
145-
}
124+
// shouldIncludeSelfEdge checks if a self-edge should be included for the given resource.
125+
// This matches the dispatcher's identity check behavior: if resource#relation appears as
126+
// a subject anywhere in the datastore (expired or not), and the filter allows it, we
127+
// include a self-edge in the results.
128+
func (a *Alias) shouldIncludeSelfEdge(ctx *Context, resource Object, filterSubjectType ObjectType) bool {
129+
// First check: does the filter allow this resource type as a subject?
130+
typeMatches := filterSubjectType.Type == "" || filterSubjectType.Type == resource.ObjectType
131+
relationMatches := filterSubjectType.Subrelation == "" || filterSubjectType.Subrelation == a.relation
132+
if !typeMatches || !relationMatches || ctx.Reader == nil {
133+
return false
134+
}
146135

147-
// Then yield remaining paths with the rewrite
148-
subPath.Relation = a.relation
149-
if !yield(subPath, nil) {
150-
return
151-
}
152-
}
153-
}
136+
// Second check: does the resource actually appear as a subject in the data?
137+
// We check for ANY relationships (expired or not) because the dispatcher's
138+
// identity check applies regardless of expiration.
139+
exists, err := a.resourceExistsAsSubject(ctx, resource)
140+
if err != nil {
141+
// On error, conservatively return false rather than failing the entire operation
142+
return false
143+
}
144+
return exists
145+
}
154146

155-
return DeduplicatePathSeq(combined), nil
147+
// resourceExistsAsSubject queries the datastore to check if the given resource appears
148+
// as a subject in any relationship, including expired relationships.
149+
func (a *Alias) resourceExistsAsSubject(ctx *Context, resource Object) (bool, error) {
150+
filter := datastore.RelationshipsFilter{
151+
OptionalSubjectsSelectors: []datastore.SubjectsSelector{{
152+
OptionalSubjectType: resource.ObjectType,
153+
OptionalSubjectIds: []string{resource.ObjectID},
154+
RelationFilter: datastore.SubjectRelationFilter{}.WithNonEllipsisRelation(a.relation),
155+
}},
156+
OptionalExpirationOption: datastore.ExpirationFilterOptionNone,
156157
}
157158

158-
// else we don't have a subpath and do the normal logic
159-
return func(yield func(Path, error) bool) {
160-
for path, err := range subSeq {
161-
if err != nil {
162-
yield(Path{}, err)
163-
return
164-
}
159+
iter, err := ctx.Reader.QueryRelationships(ctx, filter,
160+
options.WithLimit(options.LimitOne),
161+
options.WithSkipExpiration(true)) // Include expired relationships
162+
if err != nil {
163+
return false, err
164+
}
165165

166-
path.Relation = a.relation
167-
if !yield(path, nil) {
168-
return
169-
}
166+
// Check if any relationship exists
167+
for _, err := range iter {
168+
if err != nil {
169+
return false, err
170170
}
171-
}, nil
171+
return true, nil
172+
}
173+
174+
return false, nil
175+
}
176+
177+
func (a *Alias) IterResourcesImpl(ctx *Context, subject ObjectAndRelation, filterResourceType ObjectType) (PathSeq, error) {
178+
subSeq, err := ctx.IterResources(a.subIt, subject, filterResourceType)
179+
if err != nil {
180+
return nil, err
181+
}
182+
183+
// If the relation on the alias iterator is the same as the subject relation,
184+
// we have a self-edge and should yield it
185+
shouldAddSelfEdge := a.relation == subject.Relation
186+
187+
return a.maybePrependSelfEdge(GetObject(subject), subSeq, shouldAddSelfEdge), nil
172188
}
173189

174190
func (a *Alias) Clone() Iterator {

pkg/query/datastore.go

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -231,13 +231,18 @@ func (r *RelationIterator) iterSubjectsWildcardImpl(ctx *Context, resource Objec
231231
return EmptyPathSeq(), nil
232232
}
233233

234-
// Wildcard exists, so enumerate all concrete subjects for this resource.
235-
// Note: This may return some of the same subjects as the non-wildcard branch
236-
// (when both wildcard and concrete relationships exist), but the Union will
237-
// deduplicate them.
234+
// Wildcard exists, so enumerate all concrete subjects of the appropriate type.
235+
// A wildcard (e.g., user:*) means "all subjects of that type", so we need to enumerate
236+
// all defined subjects of that type in the datastore. This may return some of the same
237+
// subjects as the non-wildcard branch (when both wildcard and concrete relationships exist),
238+
// but the Union will deduplicate them.
239+
//
240+
// Note: We query for all subjects of the appropriate type, not just those with a relationship
241+
// to this specific resource. This matches the semantics of wildcards, which grant access to
242+
// ALL subjects of the type, regardless of whether they have other relationships.
238243
allSubjectsFilter := datastore.RelationshipsFilter{
239-
OptionalResourceType: r.base.DefinitionName(),
240-
OptionalResourceIds: []string{resource.ObjectID},
244+
// Note: We intentionally omit OptionalResourceType and OptionalResourceIds to find
245+
// all subjects of the appropriate type across all resources
241246
OptionalSubjectsSelectors: []datastore.SubjectsSelector{
242247
{
243248
OptionalSubjectType: r.base.Type(),

pkg/query/recursive.go

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,6 @@ import (
44
"fmt"
55

66
"github.com/google/uuid"
7-
8-
"github.com/authzed/spicedb/pkg/tuple"
97
)
108

119
const defaultMaxRecursionDepth = 50
@@ -394,15 +392,15 @@ func (r *RecursiveIterator) isRecursiveSubject(subject ObjectAndRelation) bool {
394392
return false
395393
}
396394

397-
// Must match the relation or be ellipsis/empty
398-
// Empty relation means the subject reference doesn't specify a relation
399-
// Ellipsis means "any relation on this object"
400-
if subject.Relation != r.relationName &&
401-
subject.Relation != "" &&
402-
subject.Relation != tuple.Ellipsis {
403-
return false
404-
}
405-
395+
// For IterSubjects, we should recursively expand any subject of the same type,
396+
// regardless of its specific relation. This is because:
397+
// 1. Permissions can include other relations (e.g., member = direct_member + contributor + manager)
398+
// 2. When querying engineering#direct_member, we may find applications#member as a subject,
399+
// and we need to expand it to find transitive subjects
400+
// 3. The specific relation on the subject determines what we query on that subject,
401+
// but doesn't determine whether it should be expanded
402+
//
403+
// Note: Empty relation means no relation specified, ellipsis means "any relation"
406404
return true
407405
}
408406

0 commit comments

Comments
 (0)