Files
upstream-ory-keto/schema/parser.go
upstream-mirror 9456a4e922
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
snapshot of https://github.com/ory/keto.git (master, 2026-09-16; blobs > 131072B omitted)
2026-09-16 01:08:34 +05:00

551 lines
13 KiB
Go

// 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
}