Skip to content

Commit a3bf624

Browse files
authored
Merge pull request #2553 from authzed/barakmich/wildcard
feat: add wildcard support to the plan
2 parents eebdc67 + 4fa25f6 commit a3bf624

File tree

7 files changed

+849
-84
lines changed

7 files changed

+849
-84
lines changed

internal/services/integrationtesting/query_plan_consistency_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -127,13 +127,13 @@ func runQueryPlanAssertions(t *testing.T, handle *queryPlanConsistencyHandle) {
127127

128128
switch entry.expectedPermissionship {
129129
case v1.CheckPermissionResponse_PERMISSIONSHIP_CONDITIONAL_PERMISSION:
130-
require.Equal(len(rels), 1)
130+
require.Len(rels, 1)
131131
require.NotNil(rels[0].OptionalCaveat)
132132
case v1.CheckPermissionResponse_PERMISSIONSHIP_HAS_PERMISSION:
133-
require.Equal(len(rels), 1)
133+
require.Len(rels, 1)
134134
require.Nil(rels[0].OptionalCaveat)
135135
case v1.CheckPermissionResponse_PERMISSIONSHIP_NO_PERMISSION:
136-
require.Equal(len(rels), 0)
136+
require.Len(rels, 0)
137137
}
138138
})
139139
}

pkg/query/build_tree.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,11 @@ func (b *iteratorBuilder) buildBaseRelationIterator(br *schema.BaseRelation, wit
147147
return base, nil
148148
}
149149

150+
// Wildcards represent direct access, so no subrelation processing needed
151+
if br.Wildcard {
152+
return base, nil
153+
}
154+
150155
// We must check the effective arrow of a subrelation if we have one and subrelations are enabled
151156
// (subrelations are disabled in cases of actual arrows)
152157
union := NewUnion()

pkg/query/build_tree_test.go

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -724,3 +724,100 @@ func TestBuildTreeSubrelationHandling(t *testing.T) {
724724
require.NoError(err)
725725
})
726726
}
727+
728+
func TestBuildTreeWildcardIterator(t *testing.T) {
729+
t.Parallel()
730+
731+
require := require.New(t)
732+
733+
// Create a simple schema with a wildcard relation using core types directly
734+
docDef := &corev1.NamespaceDefinition{
735+
Name: "document",
736+
Relation: []*corev1.Relation{
737+
{
738+
Name: "viewer",
739+
TypeInformation: &corev1.TypeInformation{
740+
AllowedDirectRelations: []*corev1.AllowedRelation{
741+
{
742+
Namespace: "user",
743+
RelationOrWildcard: &corev1.AllowedRelation_PublicWildcard_{
744+
PublicWildcard: &corev1.AllowedRelation_PublicWildcard{},
745+
},
746+
},
747+
},
748+
},
749+
},
750+
},
751+
}
752+
753+
userDef := &corev1.NamespaceDefinition{
754+
Name: "user",
755+
}
756+
757+
objectDefs := []*corev1.NamespaceDefinition{userDef, docDef}
758+
dsSchema, err := schema.BuildSchemaFromDefinitions(objectDefs, nil)
759+
require.NoError(err)
760+
761+
// Verify the schema has the wildcard BaseRelation
762+
documentDef := dsSchema.Definitions["document"]
763+
require.NotNil(documentDef)
764+
viewerRelation := documentDef.Relations["viewer"]
765+
require.NotNil(viewerRelation)
766+
require.Len(viewerRelation.BaseRelations, 1)
767+
baseRel := viewerRelation.BaseRelations[0]
768+
require.True(baseRel.Wildcard, "BaseRelation should have Wildcard: true")
769+
require.Equal("user", baseRel.Type)
770+
771+
// Print debug info
772+
t.Logf("BaseRelation: Type=%s, Subrelation=%s, Wildcard=%v", baseRel.Type, baseRel.Subrelation, baseRel.Wildcard)
773+
774+
t.Run("Schema with wildcard creates WildcardIterator", func(t *testing.T) {
775+
t.Parallel()
776+
it, err := BuildIteratorFromSchema(dsSchema, "document", "viewer")
777+
require.NoError(err)
778+
require.NotNil(it)
779+
780+
// Verify it's an Alias wrapping a RelationIterator with wildcard support
781+
require.IsType(&Alias{}, it)
782+
alias := it.(*Alias)
783+
require.IsType(&RelationIterator{}, alias.subIt)
784+
785+
// Check the explain output contains wildcard information
786+
explain := it.Explain()
787+
explainStr := explain.String()
788+
require.Contains(explainStr, "Relation")
789+
require.Contains(explainStr, "user:*")
790+
})
791+
792+
t.Run("Mixed wildcard and regular relations", func(t *testing.T) {
793+
t.Parallel()
794+
// Create a schema with both wildcard and regular relations
795+
mixedDocDef := namespace.Namespace(
796+
"document",
797+
namespace.MustRelation("viewer", nil,
798+
namespace.AllowedRelation("user", ""), // Regular relation
799+
namespace.AllowedPublicNamespace("user"), // Wildcard relation
800+
),
801+
)
802+
803+
mixedObjectDefs := []*corev1.NamespaceDefinition{userDef, mixedDocDef}
804+
mixedSchema, err := schema.BuildSchemaFromDefinitions(mixedObjectDefs, nil)
805+
require.NoError(err)
806+
807+
it, err := BuildIteratorFromSchema(mixedSchema, "document", "viewer")
808+
require.NoError(err)
809+
require.NotNil(it)
810+
811+
// Should create an alias with a union containing both regular and wildcard iterators
812+
require.IsType(&Alias{}, it)
813+
alias := it.(*Alias)
814+
require.IsType(&Union{}, alias.subIt)
815+
816+
// Check explain contains both relation types (regular and wildcard)
817+
explain := it.Explain()
818+
explainStr := explain.String()
819+
require.Contains(explainStr, "Union")
820+
require.Contains(explainStr, "user:...", "should contain regular relation")
821+
require.Contains(explainStr, "user:*", "should contain wildcard relation")
822+
})
823+
}

pkg/query/datastore.go

Lines changed: 108 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,20 @@ func (r *RelationIterator) buildSubjectRelationFilter() datastore.SubjectRelatio
3636
}
3737

3838
func (r *RelationIterator) CheckImpl(ctx *Context, resources []Object, subject ObjectAndRelation) (RelationSeq, error) {
39+
// If the subject type doesn't match the base relation type, return no results
40+
if subject.ObjectType != r.base.Type {
41+
return func(yield func(Relation, error) bool) {
42+
// Empty sequence
43+
}, nil
44+
}
45+
46+
if r.base.Wildcard {
47+
return r.checkWildcardImpl(ctx, resources, subject)
48+
}
49+
return r.checkNormalImpl(ctx, resources, subject)
50+
}
51+
52+
func (r *RelationIterator) checkNormalImpl(ctx *Context, resources []Object, subject ObjectAndRelation) (RelationSeq, error) {
3953
resourceIDs := make([]string, len(resources))
4054
for i, res := range resources {
4155
resourceIDs[i] = res.ObjectID
@@ -68,14 +82,101 @@ func (r *RelationIterator) CheckImpl(ctx *Context, resources []Object, subject O
6882
return RelationSeq(relIter), nil
6983
}
7084

85+
func (r *RelationIterator) checkWildcardImpl(ctx *Context, resources []Object, subject ObjectAndRelation) (RelationSeq, error) {
86+
// Query the datastore for wildcard relationships (subject ObjectID = "*")
87+
resourceIDs := make([]string, len(resources))
88+
for i, res := range resources {
89+
resourceIDs[i] = res.ObjectID
90+
}
91+
92+
filter := datastore.RelationshipsFilter{
93+
OptionalResourceType: r.base.DefinitionName(),
94+
OptionalResourceIds: resourceIDs,
95+
OptionalResourceRelation: r.base.RelationName(),
96+
OptionalSubjectsSelectors: []datastore.SubjectsSelector{
97+
{
98+
OptionalSubjectType: r.base.Type,
99+
OptionalSubjectIds: []string{tuple.PublicWildcard}, // Look for "*" subjects
100+
RelationFilter: r.buildSubjectRelationFilter(),
101+
},
102+
},
103+
}
104+
105+
reader := ctx.Datastore.SnapshotReader(ctx.Revision)
106+
107+
relIter, err := reader.QueryRelationships(ctx, filter,
108+
options.WithSkipCaveats(r.base.Caveat == ""),
109+
options.WithSkipExpiration(!r.base.Expiration),
110+
options.WithQueryShape(queryshape.CheckPermissionSelectDirectSubjects),
111+
)
112+
if err != nil {
113+
return nil, err
114+
}
115+
116+
// Transform the wildcard relationships to use the concrete subject
117+
return func(yield func(Relation, error) bool) {
118+
for rel, err := range relIter {
119+
if err != nil {
120+
if !yield(rel, err) {
121+
return
122+
}
123+
continue
124+
}
125+
126+
// Replace the wildcard subject with the concrete subject
127+
concreteRel := rel
128+
concreteRel.Subject = subject
129+
130+
if !yield(concreteRel, nil) {
131+
return
132+
}
133+
}
134+
}, nil
135+
}
136+
71137
func (r *RelationIterator) IterSubjectsImpl(ctx *Context, resource Object) (RelationSeq, error) {
138+
if r.base.Wildcard {
139+
return r.iterSubjectsWildcardImpl(ctx, resource)
140+
}
141+
return r.iterSubjectsNormalImpl(ctx, resource)
142+
}
143+
144+
func (r *RelationIterator) iterSubjectsNormalImpl(ctx *Context, resource Object) (RelationSeq, error) {
145+
filter := datastore.RelationshipsFilter{
146+
OptionalResourceType: r.base.DefinitionName(),
147+
OptionalResourceIds: []string{resource.ObjectID},
148+
OptionalResourceRelation: r.base.RelationName(),
149+
OptionalSubjectsSelectors: []datastore.SubjectsSelector{
150+
{
151+
OptionalSubjectType: r.base.Type,
152+
RelationFilter: r.buildSubjectRelationFilter(),
153+
},
154+
},
155+
}
156+
157+
reader := ctx.Datastore.SnapshotReader(ctx.Revision)
158+
159+
relIter, err := reader.QueryRelationships(ctx, filter,
160+
options.WithSkipCaveats(r.base.Caveat == ""),
161+
options.WithSkipExpiration(!r.base.Expiration),
162+
options.WithQueryShape(queryshape.AllSubjectsForResources),
163+
)
164+
if err != nil {
165+
return nil, err
166+
}
167+
168+
return RelationSeq(relIter), nil
169+
}
170+
171+
func (r *RelationIterator) iterSubjectsWildcardImpl(ctx *Context, resource Object) (RelationSeq, error) {
72172
filter := datastore.RelationshipsFilter{
73173
OptionalResourceType: r.base.DefinitionName(),
74174
OptionalResourceIds: []string{resource.ObjectID},
75175
OptionalResourceRelation: r.base.RelationName(),
76176
OptionalSubjectsSelectors: []datastore.SubjectsSelector{
77177
{
78178
OptionalSubjectType: r.base.Type,
179+
OptionalSubjectIds: []string{tuple.PublicWildcard}, // Look for "*" subjects
79180
RelationFilter: r.buildSubjectRelationFilter(),
80181
},
81182
},
@@ -106,7 +207,13 @@ func (r *RelationIterator) Clone() Iterator {
106207
}
107208

108209
func (r *RelationIterator) Explain() Explain {
210+
relationName := r.base.Subrelation
211+
if r.base.Wildcard {
212+
relationName = "*"
213+
}
109214
return Explain{
110-
Info: fmt.Sprintf("Relation(%s:%s, \"%s\", caveat: %v, expiration: %v)", r.base.DefinitionName(), r.base.RelationName(), r.base.Subrelation, r.base.Caveat != "", r.base.Expiration),
215+
Info: fmt.Sprintf("Relation(%s:%s -> %s:%s, caveat: %v, expiration: %v)",
216+
r.base.DefinitionName(), r.base.RelationName(), r.base.Type, relationName,
217+
r.base.Caveat != "", r.base.Expiration),
111218
}
112219
}

0 commit comments

Comments
 (0)