diff --git a/internal/semantic/Relation.go b/internal/semantic/Relation.go index 8752368..d58e110 100644 --- a/internal/semantic/Relation.go +++ b/internal/semantic/Relation.go @@ -168,13 +168,53 @@ func (e *ReferenceRelationExpression) ToZanzibar(r *Relation) (*core.SetOperatio } } - subrelation, ok := t.instance.relations.Get(*e.subrelation) - if !ok { - return nil, fmt.Errorf("relation %s not found on type %s.%s %w", *e.subrelation, e.relation, *e.subrelation, ErrSymbolNotFound) + var helperMethod func(tr *TypeReference) error + + helperMethod = func(tr *TypeReference) error { + + if tr.subRelation != "" { + newRelation, ok := tr.instance.relations.Get(tr.subRelation) + if !ok { + return fmt.Errorf("accessing relation %s in type %s: %w", e.relation, r.inType.name, ErrSymbolNotFound) + } + + newRelationTypes, err := newRelation.DirectTypeReferences() + if err != nil { + return err + } + + for _, nt := range newRelationTypes { + if !nt.IsResolved() { + err := nt.Resolve(r.inType) + if err != nil { + return err + } + } + + err := helperMethod(nt) + if err != nil { + return err + } else { + return nil + } + } + } + + subrelation, ok := tr.instance.relations.Get(*e.subrelation) + if !ok { + return fmt.Errorf("relation %s not found on type %s.%s %w", *e.subrelation, e.relation, *e.subrelation, ErrSymbolNotFound) + } + + if !subrelation.VisibleTo(r.inType) { + return ErrSymbolNotAccessible + } + + return nil } - if !subrelation.VisibleTo(r.inType) { - return nil, ErrSymbolNotAccessible + err := helperMethod(t) + if err != nil { + return nil, err } } return namespace.TupleToUserset(relation.SpiceDBName(), *e.subrelation), nil diff --git a/internal/semantic/Relation_test.go b/internal/semantic/Relation_test.go index 8d9ec94..c44a951 100644 --- a/internal/semantic/Relation_test.go +++ b/internal/semantic/Relation_test.go @@ -181,3 +181,108 @@ func TestAssertReferenceRelationExpressionToZanzibarSucceedsIfSubRelationOkay(t _, err = schema.ToZanzibar() assert.NoError(t, err) } + +func TestCrowsFootRelationshipHandledCorrectly(t *testing.T) { + schema := NewSchema() + namespace := NewNamespace("test_namespace", []string{}) + principal := NewType("principal", namespace, VisibilityPublic) + inner := NewType("inner", namespace, VisibilityPublic) + outer := NewType("outer", namespace, VisibilityPublic) + foo := NewType("foo", namespace, VisibilityPublic) + + innerTypeReference := NewTypeReference("", "principal", "", false) + + innerTypeReferences := []*TypeReference{innerTypeReference} + + innerRelationExpression := NewSelfRelationExpression(innerTypeReferences, CardinalityAny) + + enabledRelation, err := NewRelation("status", inner, VisibilityPublic, innerRelationExpression, nil) + assert.NoError(t, err) + + outerTypeReference := NewTypeReference("", "inner", "", false) + outerTypeReferences := []*TypeReference{outerTypeReference} + outerRelationExpression := NewSelfRelationExpression(outerTypeReferences, CardinalityAny) + + innerRelation, err := NewRelation("inner", outer, VisibilityPublic, outerRelationExpression, nil) + assert.NoError(t, err) + + outerInnerTypeReference := NewTypeReference("", "outer", "inner", false) + outerInnerTypeReferences := []*TypeReference{outerInnerTypeReference} + outterInnerRelationExpression := NewSelfRelationExpression(outerInnerTypeReferences, CardinalityAny) + + fooInnerRelation, err := NewRelation("inner", foo, VisibilityPublic, outterInnerRelationExpression, nil) + assert.NoError(t, err) + + status := "status" + innerEnabledSubRelation := NewReferenceRelationExpression("inner", &status) + innerEnabledRelation, err := NewRelation("enabled", foo, VisibilityPublic, innerEnabledSubRelation, nil) + assert.NoError(t, err) + + inner.AddRelation(enabledRelation) + outer.AddRelation(innerRelation) + foo.AddRelation(fooInnerRelation) + foo.AddRelation(innerEnabledRelation) + namespace.AddType(principal) + namespace.AddType(inner) + namespace.AddType(outer) + namespace.AddType(foo) + schema.AddNamespace(namespace) + + _, err = schema.ToZanzibar() + assert.NoError(t, err) +} + +func TestCrowsFootRelationshipErrorsWhenWrong(t *testing.T) { + schema := NewSchema() + namespace := NewNamespace("test_namespace", []string{}) + principal := NewType("principal", namespace, VisibilityPublic) + inner := NewType("inner", namespace, VisibilityPublic) + outer := NewType("outer", namespace, VisibilityPublic) + foo := NewType("foo", namespace, VisibilityPublic) + + innerTypeReference := NewTypeReference("", "principal", "", false) + + innerTypeReferences := []*TypeReference{innerTypeReference} + + innerRelationExpression := NewSelfRelationExpression(innerTypeReferences, CardinalityAny) + + enabledRelation, err := NewRelation("status", inner, VisibilityPublic, innerRelationExpression, nil) + assert.NoError(t, err) + + outerTypeReference := NewTypeReference("", "inner", "", false) + outerTypeReferences := []*TypeReference{outerTypeReference} + outerRelationExpression := NewSelfRelationExpression(outerTypeReferences, CardinalityAny) + + innerRelation, err := NewRelation("inner", outer, VisibilityPublic, outerRelationExpression, nil) + assert.NoError(t, err) + + outerInnerTypeReference := NewTypeReference("", "outer", "inner", false) + outerInnerTypeReferences := []*TypeReference{outerInnerTypeReference} + outterInnerRelationExpression := NewSelfRelationExpression(outerInnerTypeReferences, CardinalityAny) + + fooInnerRelation, err := NewRelation("inner", foo, VisibilityPublic, outterInnerRelationExpression, nil) + assert.NoError(t, err) + + enabled := "enabled" + innerEnabledSubRelation := NewReferenceRelationExpression("inner", &enabled) + innerEnabledRelation, err := NewRelation("enabled", foo, VisibilityPublic, innerEnabledSubRelation, nil) + assert.NoError(t, err) + + inner.AddRelation(enabledRelation) + outer.AddRelation(innerRelation) + foo.AddRelation(fooInnerRelation) + foo.AddRelation(innerEnabledRelation) + namespace.AddType(principal) + namespace.AddType(inner) + namespace.AddType(outer) + namespace.AddType(foo) + schema.AddNamespace(namespace) + + _, err = schema.ToZanzibar() + assert.ErrorIs(t, err, ErrSymbolNotFound) +} + +// TODO: Add tests for +// Double crows foot relationship both success and failure cases +// Crows foot where relation types are named different +// Check that inaccessible relations and types work correctly