Skip to content

Commit 6046505

Browse files
authored
feat(schema): add support for parent namespace in arrow information (#2844)
1 parent 6459f2f commit 6046505

File tree

2 files changed

+285
-4
lines changed

2 files changed

+285
-4
lines changed

pkg/schema/arrows.go

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import (
1414
type ArrowInformation struct {
1515
Arrow *core.TupleToUserset
1616
Path string
17+
ParentNamespace string
1718
ParentRelationName string
1819
}
1920

@@ -55,6 +56,14 @@ func (as *ArrowSet) HasPossibleArrowWithComputedUserset(namespaceName string, re
5556
return as.arrowsByComputedUsersetNamespaceAndRelation.Has(namespaceName + "#" + relationName)
5657
}
5758

59+
// LookupArrowsWithComputedUserset returns all arrows that have the given namespace and relation
60+
// as the computed userset.
61+
func (as *ArrowSet) LookupArrowsWithComputedUserset(namespaceName string, relationName string) []ArrowInformation {
62+
key := namespaceName + "#" + relationName
63+
found, _ := as.arrowsByComputedUsersetNamespaceAndRelation.Get(key)
64+
return found
65+
}
66+
5867
// LookupTuplesetArrows finds all arrows with the given namespace and relation name as the arrows' tupleset.
5968
func (as *ArrowSet) LookupTuplesetArrows(namespaceName string, relationName string) []ArrowInformation {
6069
key := namespaceName + "#" + relationName
@@ -79,7 +88,7 @@ func (as *ArrowSet) compute(ctx context.Context) error {
7988

8089
func (as *ArrowSet) add(ttu *core.TupleToUserset, path string, namespaceName string, relationName string) {
8190
tsKey := namespaceName + "#" + ttu.Tupleset.Relation
82-
as.arrowsByFullTuplesetRelation.Add(tsKey, ArrowInformation{Path: path, Arrow: ttu, ParentRelationName: relationName})
91+
as.arrowsByFullTuplesetRelation.Add(tsKey, ArrowInformation{Path: path, Arrow: ttu, ParentNamespace: namespaceName, ParentRelationName: relationName})
8392
}
8493

8594
func (as *ArrowSet) collectArrowInformationForRelation(ctx context.Context, def *ValidatedDefinition, relationName string) error {
@@ -115,15 +124,15 @@ func (as *ArrowSet) registerTupleToUsersetArrows(ctx context.Context, ttu *core.
115124
}
116125

117126
for _, ast := range allowedSubjectTypes {
118-
def, err := as.ts.GetValidatedDefinition(ctx, ast.Namespace)
127+
subjectDef, err := as.ts.GetValidatedDefinition(ctx, ast.Namespace)
119128
if err != nil {
120129
return err
121130
}
122131

123132
// NOTE: this is explicitly added to the arrowsByComputedUsersetNamespaceAndRelation without
124133
// checking if the relation/permission exists, because it's needed for schema diff tracking.
125-
as.arrowsByComputedUsersetNamespaceAndRelation.Add(ast.Namespace+"#"+computedUsersetRelation, ArrowInformation{Path: updatedPath, Arrow: ttu, ParentRelationName: relation.Name})
126-
if def.HasRelation(computedUsersetRelation) {
134+
as.arrowsByComputedUsersetNamespaceAndRelation.Add(ast.Namespace+"#"+computedUsersetRelation, ArrowInformation{Path: updatedPath, Arrow: ttu, ParentNamespace: def.Namespace().Name, ParentRelationName: relation.Name})
135+
if subjectDef.HasRelation(computedUsersetRelation) {
127136
as.reachableComputedUsersetRelationsByTuplesetRelation.Add(ast.Namespace+"#"+tuplesetRelation, ast.Namespace+"#"+computedUsersetRelation)
128137
}
129138
}

pkg/schema/arrows_test.go

Lines changed: 272 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -271,3 +271,275 @@ func TestAllReachableRelations(t *testing.T) {
271271
})
272272
}
273273
}
274+
275+
func TestLookupArrowsWithComputedUserset(t *testing.T) {
276+
t.Parallel()
277+
278+
type expectedArrow struct {
279+
parentNamespace string
280+
parentRelation string
281+
arrowExpression string
282+
}
283+
284+
type testcase struct {
285+
name string
286+
schemaText string
287+
288+
targetNamespace string
289+
targetRelation string
290+
expected []expectedArrow
291+
}
292+
293+
tcs := []testcase{
294+
{
295+
name: "basic arrow",
296+
schemaText: `
297+
definition user {}
298+
299+
definition organization {
300+
relation member: user
301+
}
302+
303+
definition resource {
304+
relation org: organization
305+
permission view = org->member
306+
}
307+
`,
308+
targetNamespace: "organization",
309+
targetRelation: "member",
310+
expected: []expectedArrow{
311+
{
312+
parentNamespace: "resource",
313+
parentRelation: "view",
314+
arrowExpression: "org->member",
315+
},
316+
},
317+
},
318+
{
319+
name: "multiple arrows to same relation",
320+
schemaText: `
321+
definition user {}
322+
323+
definition organization {
324+
relation member: user
325+
}
326+
327+
definition resource {
328+
relation org: organization
329+
permission view = org->member
330+
permission another_view = org->member
331+
}
332+
333+
definition another_resource {
334+
relation org: organization
335+
permission view = org->member
336+
}
337+
`,
338+
targetNamespace: "organization",
339+
targetRelation: "member",
340+
expected: []expectedArrow{
341+
{
342+
parentNamespace: "resource",
343+
parentRelation: "view",
344+
arrowExpression: "org->member",
345+
},
346+
{
347+
parentNamespace: "resource",
348+
parentRelation: "another_view",
349+
arrowExpression: "org->member",
350+
},
351+
{
352+
parentNamespace: "another_resource",
353+
parentRelation: "view",
354+
arrowExpression: "org->member",
355+
},
356+
},
357+
},
358+
{
359+
name: "arrows across different namespaces",
360+
schemaText: `
361+
definition user {}
362+
363+
definition organization {
364+
relation member: user
365+
}
366+
367+
definition project {
368+
relation org: organization
369+
permission view = org->member
370+
}
371+
372+
definition document {
373+
relation org: organization
374+
permission view = org->member
375+
}
376+
`,
377+
targetNamespace: "organization",
378+
targetRelation: "member",
379+
expected: []expectedArrow{
380+
{
381+
parentNamespace: "project",
382+
parentRelation: "view",
383+
arrowExpression: "org->member",
384+
},
385+
{
386+
parentNamespace: "document",
387+
parentRelation: "view",
388+
arrowExpression: "org->member",
389+
},
390+
},
391+
},
392+
{
393+
name: "arrow in union and exclusion expression",
394+
schemaText: `
395+
definition user {}
396+
397+
definition organization {
398+
relation member: user
399+
}
400+
401+
definition resource {
402+
relation reader: user
403+
relation writer: user
404+
relation banned: user
405+
relation org: organization
406+
permission view = reader + writer - banned + org->member
407+
}
408+
`,
409+
targetNamespace: "organization",
410+
targetRelation: "member",
411+
expected: []expectedArrow{
412+
{
413+
parentNamespace: "resource",
414+
parentRelation: "view",
415+
arrowExpression: "org->member",
416+
},
417+
},
418+
},
419+
{
420+
name: "chained arrow through intermediate type",
421+
schemaText: `
422+
definition user {}
423+
424+
definition team {
425+
relation member: user
426+
}
427+
428+
definition organization {
429+
relation team: team
430+
permission member = team->member
431+
}
432+
433+
definition resource {
434+
relation org: organization
435+
permission view = org->member
436+
}
437+
`,
438+
targetNamespace: "team",
439+
targetRelation: "member",
440+
expected: []expectedArrow{
441+
{
442+
parentNamespace: "organization",
443+
parentRelation: "member",
444+
arrowExpression: "team->member",
445+
},
446+
},
447+
},
448+
{
449+
name: "multiple arrows in nested expression",
450+
schemaText: `
451+
definition user {}
452+
453+
definition team {
454+
relation member: user
455+
}
456+
457+
definition organization {
458+
relation admin: user
459+
relation team: team
460+
permission member = admin + team->member
461+
}
462+
463+
definition resource {
464+
relation org: organization
465+
relation team: team
466+
permission view = org->member + team->member
467+
}
468+
`,
469+
targetNamespace: "team",
470+
targetRelation: "member",
471+
expected: []expectedArrow{
472+
{
473+
parentNamespace: "organization",
474+
parentRelation: "member",
475+
arrowExpression: "team->member",
476+
},
477+
{
478+
parentNamespace: "resource",
479+
parentRelation: "view",
480+
arrowExpression: "team->member",
481+
},
482+
},
483+
},
484+
{
485+
name: "arrow with intersection and exclusion",
486+
schemaText: `
487+
definition user {}
488+
489+
definition organization {
490+
relation member: user
491+
relation admin: user
492+
}
493+
494+
definition resource {
495+
relation org: organization
496+
relation blocked: user
497+
permission view = org->member & org->admin - blocked
498+
}
499+
`,
500+
targetNamespace: "organization",
501+
targetRelation: "member",
502+
expected: []expectedArrow{
503+
{
504+
parentNamespace: "resource",
505+
parentRelation: "view",
506+
arrowExpression: "org->member",
507+
},
508+
},
509+
},
510+
}
511+
512+
for _, tc := range tcs {
513+
t.Run(tc.name, func(t *testing.T) {
514+
tc := tc
515+
t.Parallel()
516+
517+
schema, err := compiler.Compile(compiler.InputSchema{
518+
Source: "",
519+
SchemaString: tc.schemaText,
520+
}, compiler.AllowUnprefixedObjectType())
521+
require.NoError(t, err)
522+
523+
res := ResolverForCompiledSchema(*schema)
524+
arrowSet, err := buildArrowSet(t.Context(), res)
525+
require.NoError(t, err)
526+
527+
arrows := arrowSet.LookupArrowsWithComputedUserset(tc.targetNamespace, tc.targetRelation)
528+
require.Len(t, arrows, len(tc.expected))
529+
530+
for _, expected := range tc.expected {
531+
found := false
532+
for _, actual := range arrows {
533+
actualExpression := actual.Arrow.Tupleset.Relation + "->" + actual.Arrow.ComputedUserset.Relation
534+
if actual.ParentNamespace == expected.parentNamespace &&
535+
actual.ParentRelationName == expected.parentRelation &&
536+
actualExpression == expected.arrowExpression {
537+
found = true
538+
break
539+
}
540+
}
541+
require.True(t, found, "expected arrow %v not found in %v", expected, arrows)
542+
}
543+
})
544+
}
545+
}

0 commit comments

Comments
 (0)