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 20, 2024
1 parent 9a61d17 commit 1569eac
Show file tree
Hide file tree
Showing 5 changed files with 217 additions and 35 deletions.
23 changes: 22 additions & 1 deletion internal/check/rewrites.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ package check

import (
"context"
"github.com/gofrs/uuid"

"github.com/pkg/errors"

Expand Down Expand Up @@ -122,6 +123,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 @@ -330,17 +332,36 @@ func (e *Engine) checkTupleToSubjectSet(
}

for _, t := range tuples {
var qObj uuid.UUID
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.Subject.UniqueID()
}
for _, child := range subjectSet.Children {
if childTupleToSubjectSet, ok := child.(*ast.TupleToSubjectSet); ok {
ns := childTupleToSubjectSet.Namespace
if childTupleToSubjectSet.Namespace == "" {
ns = tuple.Namespace
}
childRelationTuple := &relationTuple{
Namespace: ns,
Object: qObj,
Relation: childTupleToSubjectSet.Relation,
Subject: tuple.Subject,
}
g.Add(e.checkTupleToSubjectSet(childRelationTuple, childTupleToSubjectSet, restDepth-1))
}
}
}
}

resultCh <- g.Result()
}
}
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"`
Children Children `json:"children,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
175 changes: 144 additions & 31 deletions internal/schema/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ type (
namespace = internalNamespace.Namespace

parser struct {
context [][]ast.RelationType
lexer *lexer // lexer to get tokens from
namespaces []namespace // list of parsed namespaces
namespace namespace // current namespace
Expand All @@ -26,11 +27,49 @@ type (

func Parse(input string) ([]namespace, []*ParseError) {
p := &parser{
lexer: Lex("input", input),
lexer: Lex("input", input),
context: make([][]ast.RelationType, 0),
}
return p.parse()
}

func (p *parser) push(item item) {
var current []ast.RelationType = nil
if len(p.context) > 0 {
current = p.context[0]
}
if ident, ok := p.parseRelationTypeFromIdentifier(current, item); ok {
if len(p.context) > 1 {
p.context = append(p.context[:1], p.context[0:]...)
p.context[0] = ident
} else {
p.context = append([][]ast.RelationType{ident}, p.context...)
p.context[0] = ident
}
} else {
panic("wtf")
}
}

func (p *parser) pop() {
if len(p.context) == 1 {
p.context = make([][]ast.RelationType, 0)
} else if len(p.context) > 1 {
p.context = p.context[1:]
}
}

func (p *parser) peekCurrent() []ast.RelationType {
if len(p.context) == 0 {
return []ast.RelationType{{
Namespace: p.namespace.Name,
Relation: "",
}}
}

return p.context[0]
}

func (p *parser) next() (item item) {
if p.lookahead != nil {
item = *p.lookahead
Expand Down Expand Up @@ -295,6 +334,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 || (len(relations) == 1 && relations[0].Namespace == p.namespace.Name) {
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 +482,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 +504,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)
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 @@ -465,8 +529,7 @@ func (p *parser) parsePermissionExpression() (child ast.Child) {

func (p *parser) parseTupleToSubjectSet(relation item) (rewrite ast.Child) {
var (
subjectSetRel string
arg, verb item
arg, verb, name item
)
if !p.match("(") {
return nil
Expand All @@ -482,41 +545,91 @@ 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":
p.push(relation)
child := p.parseTupleToSubjectSet(name).(*ast.TupleToSubjectSet)

children := ast.Children{}
current := p.peekCurrent()
for _, childNs := range current {
children = append(children, &ast.TupleToSubjectSet{
Namespace: childNs.Namespace,
Relation: child.Relation,
ComputedSubjectSetRelation: child.ComputedSubjectSetRelation,
Children: child.Children,
})
}
p.pop()
current = p.peekCurrent()
rewrite = &ast.TupleToSubjectSet{
Namespace: current[0].Namespace,
Relation: relation.Val,
ComputedSubjectSetRelation: name.Val,
Children: children,
}

return rewrite
case "includes":
if !p.match("(", "ctx", ".", "subject", optional(","), ")") {
return nil
}
current := p.peekCurrent()
for p.matchIf(is(itemParenRight), ")") {
}
p.addCheck(checkArbitraryRelationsTypesHaveRelation(
&p.namespace, current, relation, name.Val,
))
p.addCheck(checkArbitraryNamespaceHasRelation(&p.namespace, current, relation))
rewrite = &ast.TupleToSubjectSet{
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), ")") {
}

current := p.peekCurrent()
p.addCheck(checkArbitraryRelationsTypesHaveRelation(
&p.namespace, current, relation, name.Val,
))
p.addCheck(checkArbitraryNamespaceHasRelation(&p.namespace, current, relation))
rewrite = &ast.TupleToSubjectSet{
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 1569eac

Please sign in to comment.