Files
Sebastian Spaink 4d7b5d0577 topdown: error on built-in calls with unevaluated operands (#8954)
Fixes: #3680

Built-ins require ground operands. When a Var, Ref, or comprehension
reaches one anyway, it returns a generic eval_type_error, which gets
collected into builtinErrors and turned into undefined unless strict
built-in errors are enabled -- so bugs in OPA surface as "this rule
didn't match". #3681 was this shape: a captured function output went
untracked in the save set during partial evaluation, and a variable
reached count().

Check the plugged operands first and return an internal error naming the
offending term. The check runs before the builtin-call timer starts, so
the error path needs no stopTimer. The captured output operand is
exempt: walk() is legitimately called with a non-ground composite there.

The check is shallow -- a type switch plus IsGround, a field read on
composites -- because deep-walking every operand would make
constant-time built-ins linear; benchmarks are unchanged. A nested but
ground term such as [data.foo] is therefore not detected.

---------

Signed-off-by: Sebastian Spaink <sebastianspaink@gmail.com>
2026-08-11 12:36:11 +00:00

177 lines
4.7 KiB
Go

// Copyright 2017 The OPA Authors. All rights reserved.
// Use of this source code is governed by an Apache2
// license that can be found in the LICENSE file.
package topdown
import (
"errors"
"strconv"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/open-policy-agent/opa/v1/util"
)
// Halt is a special error type that built-in function implementations return to indicate
// that policy evaluation should stop immediately.
type Halt struct {
Err error
}
func (h Halt) Error() string {
return h.Err.Error()
}
func (h Halt) Unwrap() error { return h.Err }
// Error is the error type returned by the Eval and Query functions when
// an evaluation error occurs.
type Error struct {
Code string `json:"code"`
Message string `json:"message"`
Location *ast.Location `json:"location,omitempty"`
err error `json:"-"`
}
const (
// InternalErr represents an unknown evaluation error.
InternalErr string = "eval_internal_error"
// CancelErr indicates the evaluation process was cancelled.
CancelErr string = "eval_cancel_error"
// ConflictErr indicates a conflict was encountered during evaluation. For
// instance, a conflict occurs if a rule produces multiple, differing values
// for the same key in an object. Conflict errors indicate the policy does
// not account for the data loaded into the policy engine.
ConflictErr string = "eval_conflict_error"
// TypeErr indicates evaluation stopped because an expression was applied to
// a value of an inappropriate type.
TypeErr string = "eval_type_error"
// BuiltinErr indicates a built-in function received a semantically invalid
// input or encountered some kind of runtime error, e.g., connection
// timeout, connection refused, etc.
BuiltinErr string = "eval_builtin_error"
// WithMergeErr indicates that the real and replacement data could not be merged.
WithMergeErr string = "eval_with_merge_error"
)
// IsError returns true if the err is an Error.
func IsError(err error) bool {
var e *Error
return errors.As(err, &e)
}
// IsCancel returns true if err was caused by cancellation.
func IsCancel(err error) bool {
return errors.Is(err, &Error{Code: CancelErr})
}
// Is allows matching topdown errors using errors.Is (see IsCancel).
func (e *Error) Is(target error) bool {
var t *Error
if errors.As(target, &t) {
return (t.Code == "" || e.Code == t.Code) &&
(t.Message == "" || e.Message == t.Message) &&
(t.Location == nil || t.Location.Compare(e.Location) == 0)
}
return false
}
func (e *Error) Error() string {
buf, _ := e.AppendText(make([]byte, 0, e.StringLength()))
return util.ByteSliceToString(buf)
}
func (e *Error) AppendText(buf []byte) ([]byte, error) {
if e.Location != nil {
buf, _ := e.Location.AppendText(buf)
buf = append(append(buf, ": "...), e.Code...)
buf = append(append(buf, ": "...), e.Message...)
return buf, nil
}
return append(append(append(buf, e.Code...), ": "...), e.Message...), nil
}
func (e *Error) StringLength() int {
l := len(e.Code) + 2 + len(e.Message)
if e.Location != nil {
l += e.Location.StringLength() + 2
}
return l
}
func (e *Error) Wrap(err error) *Error {
e.err = err
return e
}
func (e *Error) Unwrap() error {
return e.err
}
func functionConflictErr(loc *ast.Location) error {
return &Error{
Code: ConflictErr,
Location: loc,
Message: "functions must not produce multiple outputs for same inputs",
}
}
func completeDocConflictErr(loc *ast.Location) error {
return &Error{
Code: ConflictErr,
Location: loc,
Message: "complete rules must not produce multiple outputs",
}
}
func objectDocKeyConflictErr(loc *ast.Location) error {
return &Error{
Code: ConflictErr,
Location: loc,
Message: "object keys must be unique",
}
}
func unsupportedBuiltinErr(loc *ast.Location, name string) error {
return &Error{
Code: InternalErr,
Location: loc,
Message: "unsupported built-in: " + name,
}
}
func mergeConflictErr(loc *ast.Location) error {
return &Error{
Code: WithMergeErr,
Location: loc,
Message: "real and replacement data could not be merged",
}
}
// unevaluatedOperandErr is returned when a built-in function would have been
// called with an operand that requires evaluation, which indicates a bug in OPA
// rather than in the policy being evaluated.
func unevaluatedOperandErr(loc *ast.Location, name string, pos int, operand *ast.Term) error {
return &Error{
Code: InternalErr,
Location: loc,
Message: "built-in function " + name + " called with operand " + strconv.Itoa(pos) +
" that requires evaluation: " + operand.String(),
}
}
func internalErr(loc *ast.Location, msg string) error {
return &Error{
Code: InternalErr,
Location: loc,
Message: msg,
}
}