305 lines
7.3 KiB
Go
305 lines
7.3 KiB
Go
// Copyright 2016 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 (
|
|
"fmt"
|
|
"io"
|
|
"strings"
|
|
|
|
"github.com/open-policy-agent/opa/ast"
|
|
"github.com/open-policy-agent/opa/topdown/builtins"
|
|
)
|
|
|
|
// Op defines the types of tracing events.
|
|
type Op string
|
|
|
|
const (
|
|
// EnterOp is emitted when a new query is about to be evaluated.
|
|
EnterOp Op = "Enter"
|
|
|
|
// ExitOp is emitted when a query has evaluated to true.
|
|
ExitOp Op = "Exit"
|
|
|
|
// EvalOp is emitted when an expression is about to be evaluated.
|
|
EvalOp Op = "Eval"
|
|
|
|
// RedoOp is emitted when an expression, rule, or query is being re-evaluated.
|
|
RedoOp Op = "Redo"
|
|
|
|
// SaveOp is emitted when an expression is saved instead of evaluated
|
|
// during partial evaluation.
|
|
SaveOp Op = "Save"
|
|
|
|
// FailOp is emitted when an expression evaluates to false.
|
|
FailOp Op = "Fail"
|
|
|
|
// NoteOp is emitted when an expression invokes a tracing built-in function.
|
|
NoteOp Op = "Note"
|
|
|
|
// IndexOp is emitted during an expression evaluation to represent lookup
|
|
// matches.
|
|
IndexOp Op = "Index"
|
|
)
|
|
|
|
// VarMetadata provides some user facing information about
|
|
// a variable in some policy.
|
|
type VarMetadata struct {
|
|
Name ast.Var `json:"name"`
|
|
Location *ast.Location `json:"location"`
|
|
}
|
|
|
|
// Event contains state associated with a tracing event.
|
|
type Event struct {
|
|
Op Op // Identifies type of event.
|
|
Node ast.Node // Contains AST node relevant to the event.
|
|
Location *ast.Location // The location of the Node this event relates to.
|
|
QueryID uint64 // Identifies the query this event belongs to.
|
|
ParentID uint64 // Identifies the parent query this event belongs to.
|
|
Locals *ast.ValueMap // Contains local variable bindings from the query context.
|
|
LocalMetadata map[ast.Var]VarMetadata // Contains metadata for the local variable bindings.
|
|
Message string // Contains message for Note events.
|
|
}
|
|
|
|
// HasRule returns true if the Event contains an ast.Rule.
|
|
func (evt *Event) HasRule() bool {
|
|
_, ok := evt.Node.(*ast.Rule)
|
|
return ok
|
|
}
|
|
|
|
// HasBody returns true if the Event contains an ast.Body.
|
|
func (evt *Event) HasBody() bool {
|
|
_, ok := evt.Node.(ast.Body)
|
|
return ok
|
|
}
|
|
|
|
// HasExpr returns true if the Event contains an ast.Expr.
|
|
func (evt *Event) HasExpr() bool {
|
|
_, ok := evt.Node.(*ast.Expr)
|
|
return ok
|
|
}
|
|
|
|
// Equal returns true if this event is equal to the other event.
|
|
func (evt *Event) Equal(other *Event) bool {
|
|
if evt.Op != other.Op {
|
|
return false
|
|
}
|
|
if evt.QueryID != other.QueryID {
|
|
return false
|
|
}
|
|
if evt.ParentID != other.ParentID {
|
|
return false
|
|
}
|
|
if !evt.equalNodes(other) {
|
|
return false
|
|
}
|
|
return evt.Locals.Equal(other.Locals)
|
|
}
|
|
|
|
func (evt *Event) String() string {
|
|
return fmt.Sprintf("%v %v %v (qid=%v, pqid=%v)", evt.Op, evt.Node, evt.Locals, evt.QueryID, evt.ParentID)
|
|
}
|
|
|
|
func (evt *Event) equalNodes(other *Event) bool {
|
|
switch a := evt.Node.(type) {
|
|
case ast.Body:
|
|
if b, ok := other.Node.(ast.Body); ok {
|
|
return a.Equal(b)
|
|
}
|
|
case *ast.Rule:
|
|
if b, ok := other.Node.(*ast.Rule); ok {
|
|
return a.Equal(b)
|
|
}
|
|
case *ast.Expr:
|
|
if b, ok := other.Node.(*ast.Expr); ok {
|
|
return a.Equal(b)
|
|
}
|
|
case nil:
|
|
return other.Node == nil
|
|
}
|
|
return false
|
|
}
|
|
|
|
// Tracer defines the interface for tracing in the top-down evaluation engine.
|
|
type Tracer interface {
|
|
Enabled() bool
|
|
Trace(*Event)
|
|
}
|
|
|
|
// BufferTracer implements the Tracer interface by simply buffering all events
|
|
// received.
|
|
type BufferTracer []*Event
|
|
|
|
// NewBufferTracer returns a new BufferTracer.
|
|
func NewBufferTracer() *BufferTracer {
|
|
return &BufferTracer{}
|
|
}
|
|
|
|
// Enabled always returns true if the BufferTracer is instantiated.
|
|
func (b *BufferTracer) Enabled() bool {
|
|
if b == nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
// Trace adds the event to the buffer.
|
|
func (b *BufferTracer) Trace(evt *Event) {
|
|
*b = append(*b, evt)
|
|
}
|
|
|
|
// PrettyTrace pretty prints the trace to the writer.
|
|
func PrettyTrace(w io.Writer, trace []*Event) {
|
|
depths := depths{}
|
|
for _, event := range trace {
|
|
depth := depths.GetOrSet(event.QueryID, event.ParentID)
|
|
fmt.Fprintln(w, formatEvent(event, depth))
|
|
}
|
|
}
|
|
|
|
// PrettyTraceWithLocation prints the trace to the writer and includes location information
|
|
func PrettyTraceWithLocation(w io.Writer, trace []*Event) {
|
|
depths := depths{}
|
|
for _, event := range trace {
|
|
depth := depths.GetOrSet(event.QueryID, event.ParentID)
|
|
location := formatLocation(event)
|
|
fmt.Fprintln(w, fmt.Sprintf("%v %v", location, formatEvent(event, depth)))
|
|
}
|
|
}
|
|
|
|
func formatEvent(event *Event, depth int) string {
|
|
padding := formatEventPadding(event, depth)
|
|
if event.Op == NoteOp {
|
|
return fmt.Sprintf("%v%v %q", padding, event.Op, event.Message)
|
|
} else if event.Message != "" {
|
|
return fmt.Sprintf("%v%v %v %v", padding, event.Op, event.Node, event.Message)
|
|
} else {
|
|
switch node := event.Node.(type) {
|
|
case *ast.Rule:
|
|
return fmt.Sprintf("%v%v %v", padding, event.Op, node.Path())
|
|
default:
|
|
return fmt.Sprintf("%v%v %v", padding, event.Op, rewrite(event).Node)
|
|
}
|
|
}
|
|
}
|
|
|
|
func formatEventPadding(event *Event, depth int) string {
|
|
spaces := formatEventSpaces(event, depth)
|
|
padding := ""
|
|
if spaces > 1 {
|
|
padding += strings.Repeat("| ", spaces-1)
|
|
}
|
|
return padding
|
|
}
|
|
|
|
func formatEventSpaces(event *Event, depth int) int {
|
|
switch event.Op {
|
|
case EnterOp:
|
|
return depth
|
|
case RedoOp:
|
|
if _, ok := event.Node.(*ast.Expr); !ok {
|
|
return depth
|
|
}
|
|
}
|
|
return depth + 1
|
|
}
|
|
|
|
func formatLocation(event *Event) string {
|
|
if event.Op == NoteOp {
|
|
return fmt.Sprintf("%-19v", "note")
|
|
}
|
|
|
|
location := event.Location
|
|
if location == nil {
|
|
return fmt.Sprintf("%-19v", "")
|
|
}
|
|
|
|
if location.File == "" {
|
|
return fmt.Sprintf("%-19v", fmt.Sprintf("%.15v:%v", "query", location.Row))
|
|
}
|
|
|
|
return fmt.Sprintf("%-19v", fmt.Sprintf("%.15v:%v", location.File, location.Row))
|
|
}
|
|
|
|
// depths is a helper for computing the depth of an event. Events within the
|
|
// same query all have the same depth. The depth of query is
|
|
// depth(parent(query))+1.
|
|
type depths map[uint64]int
|
|
|
|
func (ds depths) GetOrSet(qid uint64, pqid uint64) int {
|
|
depth := ds[qid]
|
|
if depth == 0 {
|
|
depth = ds[pqid]
|
|
depth++
|
|
ds[qid] = depth
|
|
}
|
|
return depth
|
|
}
|
|
|
|
func builtinTrace(bctx BuiltinContext, args []*ast.Term, iter func(*ast.Term) error) error {
|
|
|
|
str, err := builtins.StringOperand(args[0].Value, 1)
|
|
if err != nil {
|
|
return handleBuiltinErr(ast.Trace.Name, bctx.Location, err)
|
|
}
|
|
|
|
if !traceIsEnabled(bctx.Tracers) {
|
|
return iter(ast.BooleanTerm(true))
|
|
}
|
|
|
|
evt := &Event{
|
|
Op: NoteOp,
|
|
QueryID: bctx.QueryID,
|
|
ParentID: bctx.ParentID,
|
|
Message: string(str),
|
|
}
|
|
|
|
for i := range bctx.Tracers {
|
|
bctx.Tracers[i].Trace(evt)
|
|
}
|
|
|
|
return iter(ast.BooleanTerm(true))
|
|
}
|
|
|
|
func traceIsEnabled(tracers []Tracer) bool {
|
|
for i := range tracers {
|
|
if tracers[i].Enabled() {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func rewrite(event *Event) *Event {
|
|
|
|
cpy := *event
|
|
|
|
var node ast.Node
|
|
|
|
switch v := event.Node.(type) {
|
|
case *ast.Expr:
|
|
node = v.Copy()
|
|
case ast.Body:
|
|
node = v.Copy()
|
|
case *ast.Rule:
|
|
node = v.Copy()
|
|
}
|
|
|
|
ast.TransformVars(node, func(v ast.Var) (ast.Value, error) {
|
|
if meta, ok := cpy.LocalMetadata[v]; ok {
|
|
return meta.Name, nil
|
|
}
|
|
return v, nil
|
|
})
|
|
|
|
cpy.Node = node
|
|
|
|
return &cpy
|
|
}
|
|
|
|
func init() {
|
|
RegisterBuiltinFunc(ast.Trace.Name, builtinTrace)
|
|
}
|