router/internal/expr/expr.go (180 lines of code) (raw):

package expr import ( "context" "errors" "fmt" "net/http" "net/url" "reflect" "github.com/expr-lang/expr" "github.com/expr-lang/expr/ast" "github.com/expr-lang/expr/checker" "github.com/expr-lang/expr/conf" "github.com/expr-lang/expr/file" "github.com/expr-lang/expr/parser" "github.com/expr-lang/expr/vm" "github.com/wundergraph/cosmo/router/pkg/authentication" ) /** * Naming conventions: * - Fields are named using camelCase * - Methods are named using PascalCase (Required to be exported) * - Methods should be exported through a custom type to avoid exposing accidental methods that can mutate the context * - Use interface to expose only the required methods. Blocked by https://github.com/expr-lang/expr/issues/744 * * Principles: * The Expr package is used to evaluate expressions in the context of the request or router. * The user should never be able to mutate the context or any other application state. * * Recommendations: * If possible function calls should be avoided in the expressions as they are much more expensive. * See https://github.com/expr-lang/expr/issues/734 */ const ExprRequestKey = "request" const ExprRequestAuthKey = "auth" // Context is the context for expressions parser when evaluating dynamic expressions type Context struct { Request Request `expr:"request"` // if changing the expr tag, the ExprRequestKey should be updated } // Request is the context for the request object in expressions. Be aware, that only value receiver methods // are exported in the expr environment. This is because the expressions are evaluated in a read-only context. type Request struct { Auth RequestAuth `expr:"auth"` // if changing the expr tag, the ExprRequestAuthKey should be updated URL RequestURL `expr:"url"` Header RequestHeaders `expr:"header"` Error error `expr:"error"` } // RequestURL is the context for the URL object in expressions // it is limited in scope to the URL object and its components. For convenience, the query parameters are parsed. type RequestURL struct { Method string `expr:"method"` // Scheme is the scheme of the URL Scheme string `expr:"scheme"` // Host is the host of the URL Host string `expr:"host"` // Path is the path of the URL Path string `expr:"path"` // Query is the parsed query parameters Query map[string]string `expr:"query"` } type RequestHeaders struct { Header http.Header `expr:"-"` // Do not expose the full header } // Get returns the value of the header with the given key. If the header is not present, an empty string is returned. // The key is case-insensitive and transformed to the canonical format. // TODO: Use interface to expose only the required methods. Blocked by https://github.com/expr-lang/expr/issues/744 func (r RequestHeaders) Get(key string) string { return r.Header.Get(key) } // LoadRequest loads the request object into the context. func LoadRequest(req *http.Request) Request { r := Request{ Header: RequestHeaders{ Header: req.Header, }, } m, _ := url.ParseQuery(req.URL.RawQuery) qv := make(map[string]string, len(m)) for k := range m { qv[k] = m.Get(k) } r.URL = RequestURL{ Method: req.Method, Scheme: req.URL.Scheme, Host: req.URL.Host, Path: req.URL.Path, Query: qv, } return r } type RequestAuth struct { IsAuthenticated bool `expr:"isAuthenticated"` Type string `expr:"type"` Claims map[string]any `expr:"claims"` Scopes []string `expr:"scopes"` } // LoadAuth loads the authentication context into the request object. // Must only be called when the authentication was successful. func LoadAuth(ctx context.Context) RequestAuth { authCtx := authentication.FromContext(ctx) if authCtx == nil { return RequestAuth{} } return RequestAuth{ Type: authCtx.Authenticator(), IsAuthenticated: true, Claims: authCtx.Claims(), Scopes: authCtx.Scopes(), } } func compileOptions(extra ...expr.Option) []expr.Option { options := []expr.Option{ expr.Env(Context{}), } options = append(options, extra...) return options } // CompileBoolExpression compiles an expression and returns the program. It is used for expressions that return bool. // The exprContext is used to provide the context for the expression evaluation. Not safe for concurrent use. func CompileBoolExpression(s string) (*vm.Program, error) { v, err := expr.Compile(s, compileOptions(expr.AsBool())...) if err != nil { return nil, handleExpressionError(err) } return v, nil } // CompileStringExpression compiles an expression and returns the program. It is used for expressions that return strings // The exprContext is used to provide the context for the expression evaluation. Not safe for concurrent use. func CompileStringExpression(s string) (*vm.Program, error) { v, err := expr.Compile(s, compileOptions(expr.AsKind(reflect.String))...) if err != nil { return nil, handleExpressionError(err) } return v, nil } // CompileStringExpression compiles an expression and returns the program. It is used for expressions that return strings // The exprContext is used to provide the context for the expression evaluation. Not safe for concurrent use. func CompileStringExpressionWithPatch(s string, visitor ast.Visitor) (*vm.Program, error) { v, err := expr.Compile(s, compileOptions(expr.AsKind(reflect.String), expr.Patch(visitor))...) if err != nil { return nil, handleExpressionError(err) } return v, nil } // ValidateAnyExpression compiles the expression to ensure that the expression itself is valid but more // importantly it checks if the return type is not nil and is an allowed return type // this allows us to ensure that nil and return types such as func or channels are not returned func ValidateAnyExpression(s string) error { tree, err := parser.Parse(s) if err != nil { return handleExpressionError(err) } // Check if the expression is just a nil literal if _, ok := tree.Node.(*ast.NilNode); ok { return handleExpressionError(errors.New("disallowed nil")) } config := conf.CreateNew() for _, op := range compileOptions() { op(config) } expectedType, err := checker.Check(tree, config) if err != nil { return handleExpressionError(err) } // Disallowed types switch expectedType.Kind() { case reflect.Invalid, reflect.Chan, reflect.Func: return handleExpressionError(fmt.Errorf("disallowed type: %s", expectedType.String())) } return nil } func CompileAnyExpression(s string) (*vm.Program, error) { v, err := expr.Compile(s, compileOptions()...) if err != nil { return nil, handleExpressionError(err) } return v, nil } // ResolveAnyExpression evaluates the expression and returns the result as a any. The exprContext is used to // provide the context for the expression evaluation. Not safe for concurrent use. func ResolveAnyExpression(vm *vm.Program, ctx Context) (any, error) { r, err := expr.Run(vm, ctx) if err != nil { return "", handleExpressionError(err) } return r, nil } // ResolveStringExpression evaluates the expression and returns the result as a string. The exprContext is used to // provide the context for the expression evaluation. Not safe for concurrent use. func ResolveStringExpression(vm *vm.Program, ctx Context) (string, error) { r, err := expr.Run(vm, ctx) if err != nil { return "", handleExpressionError(err) } switch v := r.(type) { case string: return v, nil default: return "", fmt.Errorf("expected string, got %T", r) } } // ResolveBoolExpression evaluates the expression and returns the result as a bool. The exprContext is used to // provide the context for the expression evaluation. Not safe for concurrent use. func ResolveBoolExpression(vm *vm.Program, ctx Context) (bool, error) { if vm == nil { return false, nil } r, err := expr.Run(vm, ctx) if err != nil { return false, handleExpressionError(err) } switch v := r.(type) { case bool: return v, nil default: return false, fmt.Errorf("failed to run expression: expected bool, got %T", r) } } func handleExpressionError(err error) error { if err == nil { return nil } var fileError *file.Error if errors.As(err, &fileError) { return fmt.Errorf("line %d, column %d: %s", fileError.Line, fileError.Column, fileError.Message) } return err }