snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
Some checks failed
CI / Run lints and checks (push) Failing after 47s
CI / Run tests (push) Failing after 1s
CI / Run race tests (push) Failing after 5s
CI / Build CLI docs (push) Has been skipped
CI / Run buf actions (push) Failing after 5s
CodeQL / Analyze (go) (push) Failing after 3s
CodeQL / Analyze (javascript) (push) Failing after 2s
Docker Image Scanners / scanners (push) Failing after 3s
Format / format (push) Failing after 3s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Draft newsletter (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
This commit is contained in:
550
schema/parser.go
Normal file
550
schema/parser.go
Normal file
@@ -0,0 +1,550 @@
|
||||
// Copyright © 2023 Ory Corp
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
package schema
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
internalNamespace "github.com/ory/keto/internal/namespace"
|
||||
"github.com/ory/keto/internal/namespace/ast"
|
||||
)
|
||||
|
||||
type (
|
||||
namespace = internalNamespace.Namespace
|
||||
|
||||
parser struct {
|
||||
lexer *lexer // lexer to get tokens from
|
||||
namespaces []namespace // list of parsed namespaces
|
||||
namespace namespace // current namespace
|
||||
errors []*ParseError // errors encountered during parsing
|
||||
fatal bool // parser encountered a fatal error
|
||||
lookahead *item // lookahead token
|
||||
checks []typeCheck // checks to perform on the namespace
|
||||
}
|
||||
)
|
||||
|
||||
func Parse(input string) ([]namespace, []*ParseError) {
|
||||
p := &parser{
|
||||
lexer: Lex("input", input),
|
||||
}
|
||||
return p.parse()
|
||||
}
|
||||
|
||||
func (p *parser) next() (item item) {
|
||||
if p.lookahead != nil {
|
||||
item = *p.lookahead
|
||||
p.lookahead = nil
|
||||
} else {
|
||||
return p.lexer.nextNonCommentItem()
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parser) peek() item {
|
||||
if p.lookahead == nil {
|
||||
i := p.lexer.nextNonCommentItem()
|
||||
p.lookahead = &i
|
||||
return i
|
||||
}
|
||||
return *p.lookahead
|
||||
}
|
||||
|
||||
func (p *parser) parse() ([]namespace, []*ParseError) {
|
||||
loop:
|
||||
for !p.fatal {
|
||||
switch item := p.next(); item.Typ {
|
||||
case itemEOF:
|
||||
break loop
|
||||
case itemError:
|
||||
p.addFatal(item, "fatal: %s", item.Val)
|
||||
case itemKeywordClass:
|
||||
p.parseClass()
|
||||
}
|
||||
}
|
||||
|
||||
if len(p.errors) == 0 {
|
||||
p.typeCheck()
|
||||
}
|
||||
|
||||
return p.namespaces, p.errors
|
||||
}
|
||||
|
||||
func (p *parser) addFatal(item item, format string, a ...interface{}) {
|
||||
p.addErr(item, format, a...)
|
||||
p.fatal = true
|
||||
}
|
||||
|
||||
func (p *parser) addErr(item item, format string, a ...interface{}) {
|
||||
err := &ParseError{
|
||||
msg: fmt.Sprintf(format, a...),
|
||||
item: item,
|
||||
p: p,
|
||||
}
|
||||
p.errors = append(p.errors, err)
|
||||
}
|
||||
|
||||
type matcher func(p *parser) (matched bool)
|
||||
|
||||
// optional optionally matches the first argument of tokens in the input. If
|
||||
// matched, the tokens are consumed. If the first token matched, all other
|
||||
// tokens must match as well.
|
||||
func optional(tokens ...string) matcher {
|
||||
return func(p *parser) bool {
|
||||
if len(tokens) == 0 {
|
||||
return true
|
||||
}
|
||||
first := tokens[0]
|
||||
if p.peek().Val == first {
|
||||
p.next()
|
||||
for _, token := range tokens[1:] {
|
||||
i := p.next()
|
||||
if i.Val != token {
|
||||
p.addFatal(i, "expected %q, got %q", token, i.Val)
|
||||
return false
|
||||
}
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
// match for the next tokens in the input.
|
||||
//
|
||||
// A token is matched depending on the type:
|
||||
// For string arguments, the input token must match the given string exactly.
|
||||
// For *string arguments, the input token must be an identifier, and the value
|
||||
// of the identifier will be written to the *string.
|
||||
// For *item arguments, the input token will be written to the pointer.
|
||||
func (p *parser) match(tokens ...interface{}) (matched bool) {
|
||||
if p.fatal {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, token := range tokens {
|
||||
switch token := token.(type) {
|
||||
case string:
|
||||
i := p.next()
|
||||
if i.Val != token {
|
||||
p.addFatal(i, "expected %q, got %q", token, i.Val)
|
||||
return false
|
||||
}
|
||||
case *string:
|
||||
i := p.next()
|
||||
if i.Typ != itemIdentifier && i.Typ != itemStringLiteral {
|
||||
p.addFatal(i, "expected identifier, got %s", i.Typ)
|
||||
return false
|
||||
}
|
||||
*token = i.Val
|
||||
case *item:
|
||||
*token = p.next()
|
||||
case matcher:
|
||||
if !token(p) {
|
||||
return false
|
||||
}
|
||||
default:
|
||||
panic(fmt.Sprintf("unexpected token type %T", token))
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
type itemPredicate func(item) bool
|
||||
|
||||
func is(typ itemType) itemPredicate {
|
||||
return func(item item) bool {
|
||||
return item.Typ == typ
|
||||
}
|
||||
}
|
||||
|
||||
// matchIf matches the tokens iff. the predicate is true.
|
||||
func (p *parser) matchIf(predicate itemPredicate, tokens ...interface{}) (matched bool) {
|
||||
if p.fatal {
|
||||
return false
|
||||
}
|
||||
if !predicate(p.peek()) {
|
||||
return false
|
||||
}
|
||||
return p.match(tokens...)
|
||||
}
|
||||
|
||||
// parseClass parses a class. The "class" token was already consumed.
|
||||
func (p *parser) parseClass() {
|
||||
var name string
|
||||
p.match(&name, "implements", "Namespace", "{")
|
||||
p.namespace = namespace{Name: name}
|
||||
|
||||
for !p.fatal {
|
||||
switch item := p.next(); {
|
||||
case item.Typ == itemBraceRight:
|
||||
p.namespaces = append(p.namespaces, p.namespace)
|
||||
return
|
||||
case item.Val == "related":
|
||||
p.parseRelated()
|
||||
case item.Val == "permits":
|
||||
p.parsePermits()
|
||||
case item.Typ == itemSemicolon:
|
||||
continue
|
||||
default:
|
||||
p.addFatal(item, "expected 'permits' or 'related', got %q", item.Val)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) parseRelated() {
|
||||
p.match(":", "{")
|
||||
for !p.fatal {
|
||||
switch item := p.next(); item.Typ {
|
||||
|
||||
case itemSemicolon:
|
||||
continue
|
||||
|
||||
case itemBraceRight:
|
||||
return
|
||||
|
||||
case itemIdentifier, itemStringLiteral:
|
||||
relation := item.Val
|
||||
var types []ast.RelationType
|
||||
p.match(":")
|
||||
|
||||
switch item := p.next(); {
|
||||
case item.Val == "Array":
|
||||
p.match("<")
|
||||
types = append(types, p.parseTypeUnion(itemAngledRight)...)
|
||||
case item.Val == "SubjectSet":
|
||||
types = append(types, p.matchSubjectSet())
|
||||
p.match("[", "]", optional(","))
|
||||
case item.Typ == itemParenLeft:
|
||||
types = append(types, p.parseTypeUnion(itemParenRight)...)
|
||||
p.match("[", "]", optional(","))
|
||||
default:
|
||||
types = append(types, ast.RelationType{Namespace: item.Val})
|
||||
p.addCheck(checkNamespaceExists(item))
|
||||
p.match("[", "]", optional(","))
|
||||
}
|
||||
|
||||
p.namespace.Relations = append(p.namespace.Relations, ast.Relation{
|
||||
Name: relation,
|
||||
Types: types,
|
||||
})
|
||||
|
||||
default:
|
||||
p.addFatal(item, "expected identifier or '}', got %s %q", item.Typ.String(), item.Val)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) matchSubjectSet() ast.RelationType {
|
||||
var namespace, relation item
|
||||
p.match("<", &namespace, ",", &relation, ">")
|
||||
p.addCheck(checkNamespaceHasRelation(namespace, relation))
|
||||
return ast.RelationType{Namespace: namespace.Val, Relation: relation.Val}
|
||||
}
|
||||
|
||||
func (p *parser) parseTypeUnion(endToken itemType) (types []ast.RelationType) {
|
||||
for !p.fatal {
|
||||
var identifier item
|
||||
p.match(&identifier)
|
||||
if identifier.Val == "SubjectSet" {
|
||||
types = append(types, p.matchSubjectSet())
|
||||
} else {
|
||||
types = append(types, ast.RelationType{Namespace: identifier.Val})
|
||||
p.addCheck(checkNamespaceExists(identifier))
|
||||
}
|
||||
switch item := p.next(); item.Typ {
|
||||
case endToken:
|
||||
return
|
||||
case itemTypeUnion:
|
||||
default:
|
||||
p.addFatal(item, "expected '|', got %q", item.Val)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parser) parsePermits() {
|
||||
p.match("=", "{")
|
||||
for !p.fatal {
|
||||
switch item := p.next(); item.Typ {
|
||||
|
||||
case itemBraceRight:
|
||||
return
|
||||
|
||||
case itemIdentifier, itemStringLiteral:
|
||||
permission := item.Val
|
||||
p.match(
|
||||
":", "(", "ctx", optional(":", "Context"), ")",
|
||||
optional(":", "boolean"), "=>",
|
||||
)
|
||||
|
||||
rewrite := simplifyExpression(p.parsePermissionExpressions(itemOperatorComma, expressionNestingMaxDepth))
|
||||
if rewrite == nil {
|
||||
return
|
||||
}
|
||||
p.namespace.Relations = append(p.namespace.Relations,
|
||||
ast.Relation{
|
||||
Name: permission,
|
||||
SubjectSetRewrite: rewrite,
|
||||
})
|
||||
|
||||
default:
|
||||
p.addFatal(item, "expected identifier or '}', got %s %q", item.Typ.String(), item.Val)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) parsePermissionExpressions(finalToken itemType, depth int) *ast.SubjectSetRewrite {
|
||||
if depth <= 0 {
|
||||
p.addFatal(p.peek(),
|
||||
"expression nested too deeply; maximal nesting depth is %d",
|
||||
expressionNestingMaxDepth)
|
||||
return nil
|
||||
}
|
||||
var root *ast.SubjectSetRewrite
|
||||
|
||||
// We only expect an expression in the beginning and after a binary
|
||||
// operator.
|
||||
expectExpression := true
|
||||
|
||||
// TODO(hperl): Split this into two state machines: One that parses an
|
||||
// expression or expression group; and one that parses a binary operator.
|
||||
for !p.fatal {
|
||||
switch item := p.peek(); item.Typ {
|
||||
|
||||
// A "(" starts a new expression group that is parsed recursively.
|
||||
case itemParenLeft:
|
||||
p.next() // consume paren
|
||||
child := p.parsePermissionExpressions(itemParenRight, depth-1)
|
||||
if child == nil {
|
||||
return nil
|
||||
}
|
||||
root = addChild(root, child)
|
||||
expectExpression = false
|
||||
|
||||
case finalToken:
|
||||
p.next() // consume final token
|
||||
return root
|
||||
|
||||
case itemBraceRight:
|
||||
// We don't consume the '}' here, to allow `parsePermits` to consume
|
||||
// it.
|
||||
return root
|
||||
|
||||
case itemOperatorAnd, itemOperatorOr:
|
||||
p.next() // consume operator
|
||||
|
||||
// A nil root means that we saw a binary expression before the first
|
||||
// expression.
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
newRoot := &ast.SubjectSetRewrite{
|
||||
Operation: setOperation(item.Typ),
|
||||
Children: []ast.Child{root},
|
||||
}
|
||||
root = newRoot
|
||||
expectExpression = true
|
||||
|
||||
// A "not" creates an AST node where the children are either a
|
||||
// single expression, or a list of expressions grouped by "()".
|
||||
case itemOperatorNot:
|
||||
p.next() // consume operator
|
||||
child := p.parseNotExpression(depth - 1)
|
||||
if child == nil {
|
||||
return nil
|
||||
}
|
||||
root = addChild(root, child)
|
||||
expectExpression = false
|
||||
|
||||
default:
|
||||
if !expectExpression {
|
||||
// Two expressions can't follow each other directly, they must
|
||||
// be separated by a binary operator.
|
||||
p.addFatal(item, "did not expect another expression")
|
||||
return nil
|
||||
}
|
||||
child := p.parsePermissionExpression()
|
||||
if child == nil {
|
||||
return nil
|
||||
}
|
||||
root = addChild(root, child)
|
||||
expectExpression = true
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *parser) parseNotExpression(depth int) ast.Child {
|
||||
if depth <= 0 {
|
||||
p.addFatal(p.peek(),
|
||||
"expression nested too deeply; maximal nesting depth is %d",
|
||||
expressionNestingMaxDepth)
|
||||
return nil
|
||||
}
|
||||
|
||||
var child ast.Child
|
||||
if item := p.peek(); item.Typ == itemParenLeft {
|
||||
p.next() // consume paren
|
||||
child = p.parsePermissionExpressions(itemParenRight, depth-1)
|
||||
} else {
|
||||
child = p.parsePermissionExpression()
|
||||
}
|
||||
if child == nil {
|
||||
return nil
|
||||
}
|
||||
return &ast.InvertResult{Child: child}
|
||||
}
|
||||
|
||||
func addChild(root *ast.SubjectSetRewrite, child ast.Child) *ast.SubjectSetRewrite {
|
||||
if root == nil {
|
||||
return child.AsRewrite()
|
||||
} else {
|
||||
root.Children = append(root.Children, child)
|
||||
return root
|
||||
}
|
||||
}
|
||||
|
||||
func setOperation(typ itemType) ast.Operator {
|
||||
switch typ {
|
||||
case itemOperatorAnd:
|
||||
return ast.OperatorAnd
|
||||
case itemOperatorOr:
|
||||
return ast.OperatorOr
|
||||
}
|
||||
panic("not reached")
|
||||
}
|
||||
|
||||
func (p *parser) matchPropertyAccess(propertyName any) bool {
|
||||
return p.matchIf(is(itemBracketLeft), "[", propertyName, "]") || p.match(".", propertyName)
|
||||
}
|
||||
|
||||
func (p *parser) parsePermissionExpression() (child ast.Child) {
|
||||
var name, verb item
|
||||
|
||||
if !p.match("this", ".", &verb) {
|
||||
return
|
||||
}
|
||||
if !p.matchPropertyAccess(&name) {
|
||||
return
|
||||
}
|
||||
|
||||
switch verb.Val {
|
||||
case "related":
|
||||
if !p.match(".") {
|
||||
return
|
||||
}
|
||||
switch item := p.next(); item.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)
|
||||
}
|
||||
|
||||
case "permits":
|
||||
if !p.match("(", "ctx", ")") {
|
||||
return
|
||||
}
|
||||
p.addCheck(checkCurrentNamespaceHasRelation(&p.namespace, name))
|
||||
return &ast.ComputedSubjectSet{Relation: name.Val}
|
||||
|
||||
default:
|
||||
p.addFatal(verb, "expected 'related' or 'permits', got %q", verb.Val)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (p *parser) parseTupleToSubjectSet(relation item) (rewrite ast.Child) {
|
||||
var (
|
||||
subjectSetRel string
|
||||
arg, verb item
|
||||
)
|
||||
if !p.match("(") {
|
||||
return nil
|
||||
}
|
||||
|
||||
switch {
|
||||
case p.matchIf(is(itemParenLeft), "(", &arg, ")"):
|
||||
case p.match(&arg):
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
p.match("=>", arg.Val, ".", &verb)
|
||||
|
||||
switch verb.Val {
|
||||
case "related":
|
||||
if !p.matchPropertyAccess(&subjectSetRel) {
|
||||
return nil
|
||||
}
|
||||
p.match(
|
||||
".", "includes", "(", "ctx", ".", "subject",
|
||||
optional(","), ")", optional(","), ")",
|
||||
)
|
||||
p.addCheck(checkAllRelationsTypesHaveRelation(
|
||||
&p.namespace, relation, subjectSetRel,
|
||||
))
|
||||
case "permits":
|
||||
if !p.matchPropertyAccess(&subjectSetRel) {
|
||||
return nil
|
||||
}
|
||||
p.match("(", "ctx", ")", ")")
|
||||
p.addCheck(checkAllRelationsTypesHaveRelation(
|
||||
&p.namespace, relation, subjectSetRel,
|
||||
))
|
||||
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,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *parser) parseComputedSubjectSet(relation item) (rewrite ast.Child) {
|
||||
if !p.match("(", "ctx", ".", "subject", ")") {
|
||||
return nil
|
||||
}
|
||||
p.addCheck(checkCurrentNamespaceHasRelation(&p.namespace, relation))
|
||||
return &ast.ComputedSubjectSet{Relation: relation.Val}
|
||||
}
|
||||
|
||||
// simplifyExpression rewrites the expression to use n-ary set operations
|
||||
// instead of binary ones.
|
||||
func simplifyExpression(root *ast.SubjectSetRewrite) *ast.SubjectSetRewrite {
|
||||
if root == nil {
|
||||
return nil
|
||||
}
|
||||
var newChildren []ast.Child
|
||||
for _, child := range root.Children {
|
||||
switch ch := child.(type) {
|
||||
case *ast.SubjectSetRewrite:
|
||||
simplifyExpression(ch)
|
||||
if len(ch.Children) == 1 || ch.Operation == root.Operation {
|
||||
// Flatten the child to parent, when:
|
||||
// - it's a single-element wrapper (AND(OR(A)) ≡ AND(A))
|
||||
// - shares the root's operation (OR(OR(A,B),C) ≡ OR(A,B,C)).
|
||||
newChildren = append(newChildren, ch.Children...)
|
||||
continue
|
||||
}
|
||||
newChildren = append(newChildren, child)
|
||||
case *ast.InvertResult:
|
||||
if innerCh, ok := ch.Child.(*ast.SubjectSetRewrite); ok {
|
||||
simplifyExpression(innerCh)
|
||||
}
|
||||
newChildren = append(newChildren, child)
|
||||
default:
|
||||
// Leaf node or different operation with multiple children; keep as-is.
|
||||
newChildren = append(newChildren, child)
|
||||
}
|
||||
}
|
||||
root.Children = newChildren
|
||||
|
||||
return root
|
||||
}
|
||||
Reference in New Issue
Block a user