Skip to content

Commit

Permalink
feat: arbitrary depth nested traversal (see ory#1131)
Browse files Browse the repository at this point in the history
warning: extremely experimental
  • Loading branch information
cmmoran committed Jan 2, 2024
1 parent 10c3aa3 commit 124da0b
Show file tree
Hide file tree
Showing 5 changed files with 200 additions and 59 deletions.
70 changes: 46 additions & 24 deletions internal/check/rewrites.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ func (e *Engine) checkSubjectSetRewrite(
Tuple: *tuple,
Type: ketoapi.TreeNodeNot,
}, e.checkInverted(ctx, tuple, c, restDepth)))

case *ast.SubjectEqualsObject:
checks = append(checks, checkgroup.WithEdge(checkgroup.Edge{
Tuple: *tuple,
Expand Down Expand Up @@ -294,7 +295,7 @@ func (e *Engine) checkComputedSubjectSet(
// * For each matching subject, then check if subject#owner@user.
func (e *Engine) checkTupleToSubjectSet(
tuple *relationTuple,
subjectSet *ast.TupleToSubjectSet,
baseSubjectSet *ast.TupleToSubjectSet,
restDepth int,
) checkgroup.CheckFunc {
if restDepth < 0 {
Expand All @@ -304,8 +305,9 @@ func (e *Engine) checkTupleToSubjectSet(

e.d.Logger().
WithField("request", tuple.String()).
WithField("tuple to subject-set relation", subjectSet.Relation).
WithField("tuple to subject-set computed", subjectSet.ComputedSubjectSetRelation).
WithField("tuple to subject-set relation", baseSubjectSet.Relation).
WithField("tuple to subject-set computed", baseSubjectSet.ComputedSubjectSetRelation).
WithField("tuple has child", baseSubjectSet.Child != nil).
Trace("check tuple to subjectSet")

return func(ctx context.Context, resultCh chan<- checkgroup.Result) {
Expand All @@ -315,31 +317,51 @@ func (e *Engine) checkTupleToSubjectSet(
err error
)
g := checkgroup.New(ctx)
for nextPage = "x"; nextPage != "" && !g.Done(); prevPage = nextPage {
tuples, nextPage, err = e.d.RelationTupleManager().GetRelationTuples(
ctx,
&query{
Namespace: &tuple.Namespace,
Object: &tuple.Object,
Relation: &subjectSet.Relation,
},
x.WithToken(prevPage))
if err != nil {
g.Add(checkgroup.ErrorFunc(err))
return
}

for _, t := range tuples {
if subSet, ok := t.Subject.(*relationtuple.SubjectSet); ok {
g.Add(e.checkIsAllowed(ctx, &relationTuple{
Namespace: subSet.Namespace,
Object: subSet.Object,
Relation: subjectSet.ComputedSubjectSetRelation,
Subject: tuple.Subject,
}, restDepth-1, false))
subjectSet := baseSubjectSet
qObj := tuple.Object
for subjectSet != nil {
for nextPage = "x"; nextPage != "" && !g.Done(); prevPage = nextPage {
if subjectSet == baseSubjectSet {
tuples, nextPage, err = e.d.RelationTupleManager().GetRelationTuples(
ctx,
&query{
Namespace: &tuple.Namespace,
Object: &qObj,
Relation: &subjectSet.Relation,
},
x.WithToken(prevPage))
} else {
tuples, nextPage, err = e.d.RelationTupleManager().GetRelationTuples(
ctx,
&query{
Namespace: &subjectSet.Namespace,
Object: &qObj,
Relation: &subjectSet.Relation,
},
x.WithToken(prevPage))
}

if err != nil {
g.Add(checkgroup.ErrorFunc(err))
return
}

for _, t := range tuples {
if subSet, ok := t.Subject.(*relationtuple.SubjectSet); ok {
qObj = subSet.Object
g.Add(e.checkIsAllowed(ctx, &relationTuple{
Namespace: subSet.Namespace,
Object: subSet.Object,
Relation: subjectSet.ComputedSubjectSetRelation,
Subject: tuple.Subject,
}, restDepth-1, false))
} else {
qObj = t.Object
}
}
}
subjectSet = subjectSet.Child
}
resultCh <- g.Result()
}
Expand Down
6 changes: 4 additions & 2 deletions internal/namespace/ast/ast_definitions.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,10 @@ type (
}

TupleToSubjectSet struct {
Relation string `json:"relation"`
ComputedSubjectSetRelation string `json:"computed_subject_set_relation"`
Namespace string `json:"namespace"`
Relation string `json:"relation"`
ComputedSubjectSetRelation string `json:"computed_subject_set_relation"`
Child *TupleToSubjectSet `json:"child,omitempty"`
}

// InvertResult inverts the check result of the child.
Expand Down
1 change: 1 addition & 0 deletions internal/persistence/sql/traverser.go
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ func (t *Traverser) TraverseSubjectSetExpansion(ctx context.Context, start *rela
rows []*subjectExpandedRelationTupleRow
limit = 1000
)
//goland:noinspection SqlType
err = t.conn.WithContext(ctx).RawQuery(fmt.Sprintf(`
SELECT current.shard_id AS shard_id,
current.subject_set_namespace AS namespace,
Expand Down
135 changes: 103 additions & 32 deletions internal/schema/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,28 @@ func (p *parser) parsePermits() {
}
}

func (p *parser) parseRelationTypeFromIdentifier(relations []ast.RelationType, ident item) ([]ast.RelationType, bool) {
if ident.Val == "" {
return []ast.RelationType{}, false
}
var search []ast.Relation
if relations == nil || len(relations) == 0 {
search = p.namespace.Relations
} else {
search = make([]ast.Relation, 0)
for _, rel := range relations {
ns, _ := namespaceQuery(p.namespaces).find(rel.Namespace)
search = append(search, ns.Relations...)
}
}

if rels, ok := relationQuery(search).find(ident.Val); ok {
return rels.Types, true
}

return []ast.RelationType{}, false
}

func (p *parser) parsePermissionExpressions(finalToken itemType, depth int) *ast.SubjectSetRewrite {
if depth <= 0 {
p.addFatal(p.peek(),
Expand Down Expand Up @@ -421,13 +443,16 @@ func (p *parser) matchPropertyAccess(propertyName any) bool {
}

func (p *parser) parsePermissionExpression() (child ast.Child) {
var name, verb item
var ctx, name, verb item

switch {
case !p.match("this"):
// 'this' or arg variable
if !p.match(&ctx) {
return
case p.matchIf(is(itemOperatorEquals), "==", "ctx", ".", "subject"):
return &ast.SubjectEqualsObject{}
}

switch {
case p.matchIf(is(itemOperatorEquals), "=="):
return p.parseComputedSubjectSet(ctx)
case !p.match(".", &verb):
return
}
Expand All @@ -440,13 +465,13 @@ func (p *parser) parsePermissionExpression() (child ast.Child) {
if !p.match(".") {
return
}
switch item := p.next(); item.Val {
switch itam := p.next(); itam.Val {
case "traverse":
child = p.parseTupleToSubjectSet(name)
child = p.parseTupleToSubjectSet(name, nil)
case "includes":
child = p.parseComputedSubjectSet(name)
default:
p.addFatal(item, "expected 'traverse' or 'includes', got %q", item.Val)
p.addFatal(itam, "expected 'traverse' or 'includes', got %q", itam.Val)
}

case "permits":
Expand All @@ -463,10 +488,9 @@ func (p *parser) parsePermissionExpression() (child ast.Child) {
return
}

func (p *parser) parseTupleToSubjectSet(relation item) (rewrite ast.Child) {
func (p *parser) parseTupleToSubjectSet(relation item, idents []ast.RelationType) (rewrite *ast.TupleToSubjectSet) {
var (
subjectSetRel string
arg, verb item
arg, verb, name item
)
if !p.match("(") {
return nil
Expand All @@ -482,41 +506,88 @@ func (p *parser) parseTupleToSubjectSet(relation item) (rewrite ast.Child) {

switch verb.Val {
case "related":
if !p.matchPropertyAccess(&subjectSetRel) {
if !p.matchPropertyAccess(&name) {
return nil
}
p.match(
".", "includes", "(", "ctx", ".", "subject",
optional(","), ")", optional(","), ")",
)
p.addCheck(checkAllRelationsTypesHaveRelation(
&p.namespace, relation, subjectSetRel,
))
if !p.match(".") {
return nil
}
switch itam := p.next(); itam.Val {
case "traverse":
childIdents, _ := p.parseRelationTypeFromIdentifier(idents, relation)
ns := ""
if len(idents) > 0 {
ns = idents[0].Namespace
}
rewrite = &ast.TupleToSubjectSet{
Namespace: ns,
Relation: relation.Val,
ComputedSubjectSetRelation: name.Val,
Child: p.parseTupleToSubjectSet(name, childIdents),
}
return rewrite
case "includes":
if !p.match("(", "ctx", ".", "subject", optional(","), ")") {
return nil
}
for p.matchIf(is(itemParenRight), ")") {
}
p.addCheck(checkArbitraryRelationsTypesHaveRelation(
&p.namespace, idents, relation, name.Val,
))
p.addCheck(checkArbitraryNamespaceHasRelation(&p.namespace, idents, relation))
ns := ""
if len(idents) > 0 {
ns = idents[0].Namespace
}
rewrite = &ast.TupleToSubjectSet{
Namespace: ns,
Relation: relation.Val,
ComputedSubjectSetRelation: name.Val,
}
}
case "permits":
if !p.matchPropertyAccess(&subjectSetRel) {
if !p.matchPropertyAccess(&name) {
return nil
}
p.match("(", "ctx", ")", ")")
p.addCheck(checkAllRelationsTypesHaveRelation(
&p.namespace, relation, subjectSetRel,
p.match("(", "ctx", ")")
for p.matchIf(is(itemParenRight), ")") {
}

p.addCheck(checkArbitraryRelationsTypesHaveRelation(
&p.namespace, idents, relation, name.Val,
))
p.addCheck(checkArbitraryNamespaceHasRelation(&p.namespace, idents, relation))
ns := ""
if len(idents) > 0 {
ns = idents[0].Namespace
}
return &ast.TupleToSubjectSet{
Namespace: ns,
Relation: relation.Val,
ComputedSubjectSetRelation: name.Val,
}
default:
p.addFatal(verb, "expected 'related' or 'permits', got %q", verb)
return nil
}
p.addCheck(checkCurrentNamespaceHasRelation(&p.namespace, relation))
return &ast.TupleToSubjectSet{
Relation: relation.Val,
ComputedSubjectSetRelation: subjectSetRel,
}

return
}

func (p *parser) parseComputedSubjectSet(relation item) (rewrite ast.Child) {
if !p.match("(", "ctx", ".", "subject", ")") {
return nil
if relation.Val == "this" {
if !p.match("ctx", ".", "subject") {
return nil
}
return &ast.SubjectEqualsObject{}
} else {
if !p.match("(", "ctx", ".", "subject", optional(","), ")") {
return nil
}
p.addCheck(checkCurrentNamespaceHasRelation(&p.namespace, relation))
return &ast.ComputedSubjectSet{Relation: relation.Val}
}
p.addCheck(checkCurrentNamespaceHasRelation(&p.namespace, relation))
return &ast.ComputedSubjectSet{Relation: relation.Val}
}

// simplifyExpression rewrites the expression to use n-ary set operations
Expand Down
47 changes: 46 additions & 1 deletion internal/schema/typechecks.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,14 @@

package schema

import "github.com/ory/keto/internal/namespace/ast"
import (
"fmt"
"github.com/ory/keto/internal/namespace/ast"
)

type (
namespaceQuery []namespace
typeQuery []ast.RelationType
relationQuery []ast.Relation
typeCheck func(p *parser)
)
Expand Down Expand Up @@ -96,6 +100,47 @@ func checkCurrentNamespaceHasRelation(current *namespace, relation item) typeChe
}
}

// checkArbitraryNamespaceHasRelation checks that the given relation exists in the
// given namespaces.
func checkArbitraryNamespaceHasRelation(namespace *namespace, relationTypes typeQuery, relation item) typeCheck {
if relationTypes == nil {
relationTypes = []ast.RelationType{{
Namespace: namespace.Name,
Relation: "",
}}
}
return func(p *parser) {
for _, ns := range relationTypes {
if n, ok := namespaceQuery(p.namespaces).find(ns.Namespace); ok {
if _, ok := relationQuery(n.Relations).find(relation.Val); ok {
return
}
p.addErr(relation,
"namespace %q did not declare relation %q",
ns.Namespace, relation.Val)
return
}
p.addErr(relation, "namespace %q was not declared", ns.Namespace)
}
}
}

func checkArbitraryRelationsTypesHaveRelation(namespace *namespace, relationTypes typeQuery, relationType item, relation string) typeCheck {
if relationTypes == nil {
relationTypes = []ast.RelationType{{
Namespace: namespace.Name,
Relation: "",
}}
}
return func(p *parser) {

for _, ns := range relationTypes {
fmt.Printf("checking %s %s %s\n", ns.Namespace, relationType.Val, relation)
recursiveCheckAllRelationsTypesHaveRelation(p, relationType, ns.Namespace, relationType.Val, relation, tupleToSubjectSetTypeCheckMaxDepth)
}
}
}

func checkAllRelationsTypesHaveRelation(current *namespace, relationType item, relation string) typeCheck {
namespace := current.Name
return func(p *parser) {
Expand Down

0 comments on commit 124da0b

Please sign in to comment.