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
551 lines
13 KiB
Go
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
|
|
}
|