Repository: cerbos/cerbos
Pull request #820 (2022-04-05 18:47:27)

enhancement: Execute policy conditions in the CEL REPL
#### Description



Patch:
diff --git a/.github/workflows/dependabot.yaml b/.github/workflows/dependabot.yaml
index f8216c5a..3bb62da6 100644
--- a/.github/workflows/dependabot.yaml
+++ b/.github/workflows/dependabot.yaml
@@ -13,7 +13,7 @@ jobs:
         with:
           ref: ${{ github.event.pull_request.head.ref }}
 
-      - uses: actions/setup-go@v2
+      - uses: actions/setup-go@v3
         with:
           go-version: 1.18.x
 
diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml
index 7205ca91..e88b5bca 100644
--- a/.github/workflows/release.yaml
+++ b/.github/workflows/release.yaml
@@ -10,7 +10,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Install Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: 1.18.x
 
diff --git a/.github/workflows/snaphot.yaml b/.github/workflows/snaphot.yaml
index d2b4bad6..983648fb 100644
--- a/.github/workflows/snaphot.yaml
+++ b/.github/workflows/snaphot.yaml
@@ -12,7 +12,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Install Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: 1.18.x
 
@@ -63,7 +63,7 @@ jobs:
         run: make test-all
 
       - name: Upload to CodeCov
-        uses: codecov/codecov-action@v2
+        uses: codecov/codecov-action@v3
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
           files: "unit.cover,integration.cover"
diff --git a/cmd/cerbos/repl/internal/cel.go b/cmd/cerbos/repl/internal/cel.go
index fa8ddcc4..544f8efa 100644
--- a/cmd/cerbos/repl/internal/cel.go
+++ b/cmd/cerbos/repl/internal/cel.go
@@ -6,11 +6,12 @@ package internal
 import (
 	"fmt"
 
-	enginev1 "github.com/cerbos/cerbos/api/genpb/cerbos/engine/v1"
-	"github.com/cerbos/cerbos/internal/conditions"
 	"github.com/google/cel-go/checker/decls"
 	"github.com/google/cel-go/common/types"
 	exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
+
+	enginev1 "github.com/cerbos/cerbos/api/genpb/cerbos/engine/v1"
+	"github.com/cerbos/cerbos/internal/conditions"
 )
 
 const lastResultVar = "_"
diff --git a/cmd/cerbos/repl/internal/directive.go b/cmd/cerbos/repl/internal/directive.go
index f0d5508e..9f81fb81 100644
--- a/cmd/cerbos/repl/internal/directive.go
+++ b/cmd/cerbos/repl/internal/directive.go
@@ -6,7 +6,7 @@ package internal
 import (
 	"fmt"
 
-	participle "github.com/alecthomas/participle/v2"
+	"github.com/alecthomas/participle/v2"
 	"github.com/alecthomas/participle/v2/lexer"
 )
 
@@ -14,6 +14,9 @@ func NewParser() (*participle.Parser, error) {
 	lex, err := lexer.New(lexer.Rules{
 		"Root": {
 			{Name: "Ident", Pattern: `[a-zA-Z]\w*(\.\w+)*`},
+			{Name: "#", Pattern: `#`},
+			{Name: "Int", Pattern: `[0-9]+`},
+			{Name: "Path", Pattern: `(?:((?:[\/]?)(?:[^\/]+\/)+)([^\/]+))`},
 			{Name: "Assign", Pattern: `=`, Action: lexer.Push("Assign")},
 			{Name: "Whitespace", Pattern: `\s+`},
 		},
@@ -38,14 +41,25 @@ func NewParser() (*participle.Parser, error) {
 
 //nolint:govet
 type REPLDirective struct {
-	Exit  bool          `parser:"@('q'|'quit'|'exit')"`
-	Reset bool          `parser:"| @'reset'"`
-	Vars  bool          `parser:"| @'vars'"`
-	Help  bool          `parser:"| @('h' | 'help')"`
-	Let   *LetDirective `parser:"| @@"`
+	Exit  bool           `parser:"@('q'|'quit'|'exit')"`
+	Reset bool           `parser:"| @'reset'"`
+	Vars  bool           `parser:"| @'vars'"`
+	Help  bool           `parser:"| @('h' | 'help')"`
+	Rules bool           `parser:"| @'rules'"`
+	Load  *LoadDirective `parser:"| @@"`
+	Exec  *ExecDirective `parser:"| @@"`
+	Let   *LetDirective  `parser:"| @@"`
 }
 
 type LetDirective struct {
 	Name string `parser:"'let' @Ident"`
 	Expr string `parser:"'=' @Any"`
 }
+
+type LoadDirective struct {
+	Path string `parser:"'load' @Path"`
+}
+
+type ExecDirective struct {
+	RuleID int `parser:"'exec' '#'@Int"`
+}
diff --git a/cmd/cerbos/repl/internal/eval.go b/cmd/cerbos/repl/internal/eval.go
new file mode 100644
index 00000000..db5dc847
--- /dev/null
+++ b/cmd/cerbos/repl/internal/eval.go
@@ -0,0 +1,82 @@
+// Copyright 2021-2022 Zenauth Ltd.
+// SPDX-License-Identifier: Apache-2.0
+
+package internal
+
+import (
+	"errors"
+	"fmt"
+
+	"github.com/pterm/pterm"
+
+	"github.com/cerbos/cerbos/internal/printer/colored"
+)
+
+var errEvalErrorPresent = errors.New("failed expression(s) present")
+
+type evalOutput struct {
+	tree  pterm.LeveledList
+	level int
+}
+
+func (eo *evalOutput) append(text string) {
+	eo.tree = append(eo.tree, pterm.LeveledListItem{Level: eo.level, Text: text})
+}
+
+func (eo *evalOutput) appendAndLevelUp(text string) {
+	eo.append(text)
+	eo.level++
+}
+
+func buildEvalOutput(e *eval) *evalOutput {
+	eo := &evalOutput{level: 0}
+	doBuildEvalOutput(eo, e)
+
+	return eo
+}
+
+func successText(success bool) string {
+	if success {
+		return colored.REPLSuccess("true")
+	}
+
+	return colored.REPLError("false")
+}
+
+func doBuildEvalOutput(eo *evalOutput, e *eval) {
+	switch e.evalType {
+	case evalTypeExpr:
+		eo.append(fmt.Sprintf("%s [%v]", colored.REPLExpr(e.expr), successText(e.success)))
+	case evalTypeAny, evalTypeAll, evalTypeNone:
+		eo.appendAndLevelUp(fmt.Sprintf("%s [%v]", e.evalType, successText(e.success)))
+		for _, ev := range e.evals {
+			doBuildEvalOutput(eo, ev)
+		}
+	}
+}
+
+type evalType string
+
+const (
+	evalTypeAll  evalType = "all"
+	evalTypeAny  evalType = "any"
+	evalTypeExpr evalType = "expr"
+	evalTypeNone evalType = "none"
+)
+
+type eval struct {
+	err      error
+	evalType evalType
+	expr     string
+	evals    []*eval
+	success  bool
+}
+
+func (e *eval) append(eval *eval) {
+	if eval.err != nil || !eval.success {
+		e.err = errEvalErrorPresent
+		e.success = false
+	}
+
+	e.evals = append(e.evals, eval)
+}
diff --git a/cmd/cerbos/repl/internal/help.txt b/cmd/cerbos/repl/internal/help.txt
index 9bdd6b24..e9199044 100644
--- a/cmd/cerbos/repl/internal/help.txt
+++ b/cmd/cerbos/repl/internal/help.txt
@@ -16,6 +16,9 @@ Directives are commands prefixed with a colon (":") character. The following dir
   :quit             | Exit the program
   :reset            | Clear all variables
   :vars             | View defined variables
+  :load             | Load rules from a policy
+  :rules            | View rules loaded from policies
+  :exec             | Execute rules loaded from policies
 
 ## Evaluating CEL conditions
 You can type in any valid CEL expression at the prompt to immediately evaluate it.
@@ -50,6 +53,23 @@ You can view the currently defined variables by using the :vars directive.
   :vars             | prints all defined variables
 
 
+## Load rules from a policy
+You can load rules from a Cerbos policy using the :load directive.
+
+  :load /policies/resource_policies/policy_01.yaml
+
+## View loaded rules
+You can view the currently loaded policy rules using the :rules directive. Each rule has an ID assigned by the REPL. You can use that ID in an :exec directive to execute the condition attached to that rule. 
+
+  :rules            | prints currently loaded policy rules
+
+## Execute rules
+
+You can execute a loaded rule using its ID. Use the :rules directive to view the IDs for rules.
+
+  :exec  #1           |  execute rule with ID #1
+
+
 ## Clear state
 You can destroy all variables and reset the REPL with the :reset directive.
 
diff --git a/cmd/cerbos/repl/internal/repl.go b/cmd/cerbos/repl/internal/repl.go
index 8cdea6a8..97e8e2f7 100644
--- a/cmd/cerbos/repl/internal/repl.go
+++ b/cmd/cerbos/repl/internal/repl.go
@@ -4,22 +4,17 @@
 package internal
 
 import (
+	_ "embed"
 	"encoding/json"
 	"errors"
 	"fmt"
 	"io"
+	"os"
 	"reflect"
 	"sort"
 	"strings"
 
-	_ "embed"
-
-	participle "github.com/alecthomas/participle/v2"
-	enginev1 "github.com/cerbos/cerbos/api/genpb/cerbos/engine/v1"
-	"github.com/cerbos/cerbos/internal/conditions"
-	"github.com/cerbos/cerbos/internal/outputcolor"
-	"github.com/cerbos/cerbos/internal/printer"
-	"github.com/cerbos/cerbos/internal/printer/colored"
+	"github.com/alecthomas/participle/v2"
 	"github.com/google/cel-go/cel"
 	"github.com/google/cel-go/checker/decls"
 	"github.com/google/cel-go/common"
@@ -27,9 +22,21 @@ import (
 	"github.com/google/cel-go/common/types/ref"
 	"github.com/google/cel-go/interpreter"
 	"github.com/peterh/liner"
+	"github.com/pterm/pterm"
 	exprpb "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
 	"google.golang.org/protobuf/encoding/protojson"
 	"google.golang.org/protobuf/proto"
+
+	enginev1 "github.com/cerbos/cerbos/api/genpb/cerbos/engine/v1"
+	policyv1 "github.com/cerbos/cerbos/api/genpb/cerbos/policy/v1"
+	runtimev1 "github.com/cerbos/cerbos/api/genpb/cerbos/runtime/v1"
+	"github.com/cerbos/cerbos/internal/compile"
+	"github.com/cerbos/cerbos/internal/conditions"
+	"github.com/cerbos/cerbos/internal/namer"
+	"github.com/cerbos/cerbos/internal/outputcolor"
+	"github.com/cerbos/cerbos/internal/policy"
+	"github.com/cerbos/cerbos/internal/printer"
+	"github.com/cerbos/cerbos/internal/printer/colored"
 )
 
 var (
@@ -49,16 +56,20 @@ const (
 	commentPrefix   = '#'
 	directivePrefix = ':'
 	prompt          = "-> "
+	rulePrefix      = "#"
 	secondaryPrompt = "> "
+	yamlIndent      = 2
 )
 
 type REPL struct {
-	vars     variables
-	decls    map[string]*exprpb.Decl
-	reader   *liner.State
-	parser   *participle.Parser
-	output   Output
-	toRefVal func(any) ref.Val
+	policyKey string
+	output    Output
+	vars      variables
+	decls     map[string]*exprpb.Decl
+	reader    *liner.State
+	parser    *participle.Parser
+	toRefVal  func(any) ref.Val
+	rules     []any
 }
 
 func NewREPL(reader *liner.State, output Output) (*REPL, error) {
@@ -158,12 +169,18 @@ func (r *REPL) processDirective(line string) error {
 		return r.showVars()
 	case directive.Help:
 		return r.help()
+	case directive.Rules:
+		return r.showRules()
 	case directive.Let != nil:
 		prefix, _, _ := strings.Cut(directive.Let.Name, ".")
 		if _, ok := specialVars[prefix]; ok {
 			return r.setSpecialVar(directive.Let.Name, directive.Let.Expr)
 		}
 		return r.processExpr(directive.Let.Name, directive.Let.Expr)
+	case directive.Load != nil:
+		return r.loadRulesFromPolicy(directive.Load.Path)
+	case directive.Exec != nil:
+		return r.execRule(directive.Exec.RuleID)
 	default:
 		return fmt.Errorf("unknown directive %q", line)
 	}
@@ -198,6 +215,23 @@ func (r *REPL) showVars() error {
 	return nil
 }
 
+func (r *REPL) showRules() error {
+	if r.rules == nil {
+		r.output.Println()
+		return nil
+	}
+
+	r.output.Print(fmt.Sprintf("Showing rules for the policy '%s'\n", colored.REPLPolicyName(r.policyKey)))
+	for idx, rule := range r.rules {
+		err := r.output.PrintRule(idx, rule)
+		if err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
 func (r *REPL) setSpecialVar(name, value string) error {
 	switch name {
 	case lastResultVar:
@@ -318,6 +352,150 @@ func (r *REPL) evalExpr(expr string) (ref.Val, *exprpb.Type, error) {
 	return val, tpe, nil
 }
 
+func (r *REPL) loadRulesFromPolicy(path string) error {
+	f, err := os.Open(strings.Trim(path, " "))
+	if err != nil {
+		return fmt.Errorf("failed to open policy file at %s: %w", path, err)
+	}
+	defer f.Close()
+
+	p, err := policy.ReadPolicy(f)
+	if err != nil {
+		return fmt.Errorf("failed to read policy file: %w", err)
+	}
+
+	r.vars[conditions.CELVariablesIdent] = r.toRefVal(p.Variables)
+	r.vars[conditions.CELVariablesAbbrev] = r.toRefVal(p.Variables)
+
+	r.rules = nil
+	switch pt := p.PolicyType.(type) {
+	case *policyv1.Policy_ResourcePolicy:
+		for _, rule := range pt.ResourcePolicy.Rules {
+			if rule.Condition != nil && rule.Condition.Condition != nil {
+				_, ok := rule.Condition.Condition.(*policyv1.Condition_Match)
+				if !ok {
+					continue
+				}
+				r.rules = append(r.rules, rule)
+			}
+		}
+
+		r.policyKey = namer.PolicyKey(p)
+		r.output.Println(fmt.Sprintf("Resource policy '%s' loaded", colored.REPLPolicyName(namer.PolicyKey(p))))
+	case *policyv1.Policy_DerivedRoles:
+		for _, def := range pt.DerivedRoles.Definitions {
+			if def.Condition != nil {
+				r.rules = append(r.rules, def)
+			}
+		}
+
+		r.policyKey = namer.PolicyKey(p)
+		r.output.Println(fmt.Sprintf("Derived roles '%s' loaded", colored.REPLPolicyName(namer.PolicyKey(p))))
+	case *policyv1.Policy_PrincipalPolicy:
+		for _, rule := range pt.PrincipalPolicy.Rules {
+			for _, action := range rule.Actions {
+				if action.Condition != nil {
+					pr := &policyv1.PrincipalRule{
+						Resource: rule.Resource,
+						Actions: []*policyv1.PrincipalRule_Action{
+							action,
+						},
+					}
+					r.rules = append(r.rules, pr)
+				}
+			}
+		}
+
+		r.policyKey = namer.PolicyKey(p)
+		r.output.Println(fmt.Sprintf("Principal policy '%s' loaded", colored.REPLPolicyName(namer.PolicyKey(p))))
+	}
+
+	r.output.Println()
+	err = r.showRules()
+	if err != nil {
+		return fmt.Errorf("failed to display rules after loading a policy: %w", err)
+	}
+
+	return nil
+}
+
+func (r *REPL) execRule(id int) error {
+	if id >= len(r.rules) {
+		return fmt.Errorf("failed to find rule with id %d", id)
+	}
+
+	return r.evalCondition(id)
+}
+
+func (r *REPL) evalCondition(id int) error {
+	var cond *policyv1.Condition
+	switch rt := r.rules[id].(type) {
+	case *policyv1.ResourceRule:
+		cond = rt.Condition
+	case *policyv1.RoleDef:
+		cond = rt.Condition
+	case *policyv1.PrincipalRule:
+		cond = rt.Actions[0].Condition
+	}
+
+	condition, err := compile.Condition(cond)
+	if err != nil {
+		return fmt.Errorf("failed to compile condition: %w", err)
+	}
+
+	e := r.doEvalCondition(condition)
+	eo := buildEvalOutput(e)
+
+	err = pterm.DefaultTree.WithRoot(pterm.NewTreeFromLeveledList(eo.tree)).Render()
+	if err != nil {
+		return fmt.Errorf("failed to render tree: %w", err)
+	}
+
+	return nil
+}
+
+func (r *REPL) doEvalCondition(condition *runtimev1.Condition) *eval {
+	switch c := condition.Op.(type) {
+	case *runtimev1.Condition_Expr:
+		val, tpe, err := r.evalExpr(c.Expr.Original)
+		if err != nil {
+			return &eval{err: err, success: false, evalType: evalTypeExpr, evals: nil, expr: c.Expr.Original}
+		}
+
+		r.vars[lastResultVar] = val
+		r.decls[lastResultVar] = decls.NewVar(lastResultVar, tpe)
+
+		if success, ok := val.Value().(bool); ok {
+			return &eval{err: nil, success: success, evalType: evalTypeExpr, evals: nil, expr: c.Expr.Original}
+		}
+
+		return &eval{err: err, success: false, evalType: evalTypeExpr, evals: nil, expr: c.Expr.Original}
+	case *runtimev1.Condition_All:
+		eval := &eval{err: nil, success: true, evalType: evalTypeAll, evals: nil}
+		for _, expr := range c.All.GetExpr() {
+			e := r.doEvalCondition(expr)
+			eval.append(e)
+		}
+		return eval
+	case *runtimev1.Condition_Any:
+		eval := &eval{err: nil, success: true, evalType: evalTypeAny, evals: nil}
+		for _, expr := range c.Any.GetExpr() {
+			e := r.doEvalCondition(expr)
+			eval.append(e)
+		}
+		return eval
+	case *runtimev1.Condition_None:
+		eval := &eval{err: nil, success: true, evalType: evalTypeNone, evals: nil}
+		for _, expr := range c.None.GetExpr() {
+			e := r.doEvalCondition(expr)
+			eval.append(e)
+		}
+		return eval
+	}
+
+	return nil
+}
+
 func (r *REPL) mkEnv() (*cel.Env, error) {
 	decls := make([]*exprpb.Decl, len(r.decls))
 	i := 0
@@ -345,7 +523,9 @@ type Output interface {
 	Print(string, ...any)
 	Println(...any)
 	PrintResult(string, ref.Val)
+	PrintRule(int, any) error
 	PrintJSON(any)
+	PrintYAML(proto.Message, int)
 	PrintErr(string, error)
 }
 
@@ -366,6 +546,18 @@ func (po *PrinterOutput) Print(format string, args ...any) {
 	po.Println()
 }
 
+func (po *PrinterOutput) PrintRule(id int, rule any) error {
+	msg, ok := rule.(proto.Message)
+	if !ok {
+		return fmt.Errorf("failed to type assert rule with id: %s", fmt.Sprintf("%s%d", rulePrefix, id))
+	}
+
+	po.Println(fmt.Sprintf("[%s]", colored.REPLRule(fmt.Sprintf("%s%d", rulePrefix, id))))
+	po.PrintYAML(msg, yamlIndent)
+
+	return nil
+}
+
 func (po *PrinterOutput) PrintResult(name string, value ref.Val) {
 	po.Printf("%s = ", colored.REPLVar(name))
 
@@ -406,6 +598,13 @@ func (po *PrinterOutput) PrintJSON(obj any) {
 	po.Println()
 }
 
+func (po *PrinterOutput) PrintYAML(obj proto.Message, indent int) {
+	if err := po.Printer.PrintProtoYAML(obj, po.level, indent); err != nil {
+		po.Println("<...>")
+	}
+	po.Println()
+}
+
 func (po *PrinterOutput) PrintErr(msg string, err error) {
 	po.Println(colored.REPLError(fmt.Sprintf("Error: %s", msg)))
 	if err != nil {
diff --git a/cmd/cerbos/repl/repl.go b/cmd/cerbos/repl/repl.go
index 96b07d07..944dca91 100644
--- a/cmd/cerbos/repl/repl.go
+++ b/cmd/cerbos/repl/repl.go
@@ -6,20 +6,28 @@ package repl
 import (
 	"errors"
 	"fmt"
+	"io"
 	"os"
 	"path/filepath"
 
 	"github.com/adrg/xdg"
 	"github.com/alecthomas/kong"
-	"github.com/cerbos/cerbos/cmd/cerbos/repl/internal"
 	"github.com/peterh/liner"
+
+	"github.com/cerbos/cerbos/cmd/cerbos/repl/internal"
 )
 
 type Cmd struct {
 	History string `help:"Path to history file" type:"path"`
 }
 
+func (c *Cmd) clear(stdout io.Writer) {
+	fmt.Fprintf(stdout, "\033[H\033[2J")
+}
+
 func (c *Cmd) Run(k *kong.Kong) error {
+	c.clear(k.Stdout)
+
 	histFile := getHistoryFile(c.History)
 
 	reader := liner.NewLiner()
diff --git a/docs/modules/cli/pages/cerbos.adoc b/docs/modules/cli/pages/cerbos.adoc
index f731743a..e8c358d8 100644
--- a/docs/modules/cli/pages/cerbos.adoc
+++ b/docs/modules/cli/pages/cerbos.adoc
@@ -203,6 +203,41 @@ Type `:help` or `:h` to display help. Type `:quit`, `:q` or `:exit` to exit the
 
 TIP: Use the up/down arrow keys (or kbd:[Ctrl+P]/kbd:[Ctrl+N]) to navigate command history. Most of the standard line-editing commands such as kbd:[Ctrl+a], kbd:[Ctrl+h], kbd:[Ctrl+r] are supported as well. 
 
+You can load rules from a policies.
+
+[listing]
+----
+-> :load /policies/resource_policies/policy_01.yaml
+Resource policy 'resource.leave_request.v20210210' loaded
+
+-> :load /policies/derived_roles/derived_01.yaml
+Derived roles 'derived_roles.alpha' loaded
+----
+
+Type `:rules` to display the rules loaded from policies.
+
+Type `:exec` to execute the rules using the environment of the REPL.
+
+[listing]
+----
+-> :rules
+#0 (derived_roles)
+{
+  "name": "employee_that_owns_the_record",
+  "parentRoles": [
+    "employee"
+  ],
+  "condition": {
+    "match": {
+      "expr": "R.attr.owner == P.id"
+    }
+  }
+}
+
+-> :exec #0
+Executing: R.attr.owner == P.id
+_ = true
+----
 
 [#run]
 == `run` Command
diff --git a/go.mod b/go.mod
index a6f61e25..39676c8f 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,7 @@ require (
 	github.com/adrg/xdg v0.4.0
 	github.com/alecthomas/chroma v0.10.0
 	github.com/alecthomas/kong v0.5.0
-	github.com/alecthomas/participle/v2 v2.0.0-alpha7
+	github.com/alecthomas/participle/v2 v2.0.0-alpha8
 	github.com/aws/aws-sdk-go v1.43.31
 	github.com/bluele/gcache v0.0.2
 	github.com/cespare/xxhash v1.1.0
@@ -42,7 +42,7 @@ require (
 	github.com/jwalton/gchalk v1.3.0
 	github.com/jwalton/go-supportscolor v1.1.0
 	github.com/kavu/go_reuseport v1.5.0
-	github.com/lestrrat-go/jwx v1.2.21
+	github.com/lestrrat-go/jwx v1.2.22
 	github.com/mattn/go-isatty v0.0.14
 	github.com/minio/minio-go/v7 v7.0.23
 	github.com/oklog/ulid/v2 v2.0.2
@@ -62,12 +62,12 @@ require (
 	github.com/tidwall/sjson v1.2.4
 	go.elastic.co/ecszap v1.0.1
 	go.opencensus.io v0.23.0
-	go.opentelemetry.io/otel v1.6.1
+	go.opentelemetry.io/otel v1.6.3
 	go.opentelemetry.io/otel/bridge/opencensus v0.28.0
-	go.opentelemetry.io/otel/exporters/jaeger v1.6.1
-	go.opentelemetry.io/otel/sdk v1.6.1
-	go.opentelemetry.io/otel/trace v1.6.1
-	go.uber.org/automaxprocs v1.4.0
+	go.opentelemetry.io/otel/exporters/jaeger v1.6.3
+	go.opentelemetry.io/otel/sdk v1.6.3
+	go.opentelemetry.io/otel/trace v1.6.3
+	go.uber.org/automaxprocs v1.5.1
 	go.uber.org/config v1.4.0
 	go.uber.org/multierr v1.8.0
 	go.uber.org/zap v1.21.0
@@ -82,7 +82,7 @@ require (
 	gopkg.in/segmentio/analytics-go.v3 v3.1.0
 	gopkg.in/yaml.v2 v2.4.0
 	helm.sh/helm/v3 v3.8.1
-	modernc.org/sqlite v1.15.4
+	modernc.org/sqlite v1.16.0
 )
 
 require (
@@ -170,7 +170,7 @@ require (
 	github.com/klauspost/cpuid v1.3.1 // indirect
 	github.com/lestrrat-go/backoff/v2 v2.0.8 // indirect
 	github.com/lestrrat-go/blackmagic v1.0.0 // indirect
-	github.com/lestrrat-go/httpcc v1.0.0 // indirect
+	github.com/lestrrat-go/httpcc v1.0.1 // indirect
 	github.com/lestrrat-go/iter v1.0.1 // indirect
 	github.com/lestrrat-go/option v1.0.0 // indirect
 	github.com/lib/pq v1.10.4 // indirect
diff --git a/go.sum b/go.sum
index 0e3a9eb5..61a0035c 100644
--- a/go.sum
+++ b/go.sum
@@ -200,8 +200,8 @@ github.com/alecthomas/chroma v0.10.0 h1:7XDcGkCQopCNKjZHfYrNLraA+M7e0fMiJ/Mfikbf
 github.com/alecthomas/chroma v0.10.0/go.mod h1:jtJATyUxlIORhUOFNA9NZDWGAQ8wpxQQqNSB4rjA/1s=
 github.com/alecthomas/kong v0.5.0 h1:u8Kdw+eeml93qtMZ04iei0CFYve/WPcA5IFh+9wSskE=
 github.com/alecthomas/kong v0.5.0/go.mod h1:uzxf/HUh0tj43x1AyJROl3JT7SgsZ5m+icOv1csRhc0=
-github.com/alecthomas/participle/v2 v2.0.0-alpha7 h1:cK4vjj0VSgb3lN1nuKA5F7dw+1s1pWBe5bx7nNCnN+c=
-github.com/alecthomas/participle/v2 v2.0.0-alpha7/go.mod h1:NumScqsC42o9x+dGj8/YqsIfhrIQjFEOFovxotbBirA=
+github.com/alecthomas/participle/v2 v2.0.0-alpha8 h1:X6nuChfgfQXNTE+ZdjTFSfnSNr8E07LSVLAqIIjtsGI=
+github.com/alecthomas/participle/v2 v2.0.0-alpha8/go.mod h1:NumScqsC42o9x+dGj8/YqsIfhrIQjFEOFovxotbBirA=
 github.com/alecthomas/repr v0.0.0-20181024024818-d37bc2a10ba1/go.mod h1:xTS7Pm1pD1mvyM075QCDSRqH6qRLXylzS24ZTpRiSzQ=
 github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142 h1:8Uy0oSf5co/NZXje7U1z8Mpep++QJOldL2hs/sBQf48=
 github.com/alecthomas/repr v0.0.0-20210801044451-80ca428c5142/go.mod h1:2kn6fqh/zIyPLmm3ugklbEi5hg5wS435eygvNfaDQL8=
@@ -1141,12 +1141,12 @@ github.com/lestrrat-go/backoff/v2 v2.0.8 h1:oNb5E5isby2kiro9AgdHLv5N5tint1AnDVVf
 github.com/lestrrat-go/backoff/v2 v2.0.8/go.mod h1:rHP/q/r9aT27n24JQLa7JhSQZCKBBOiM/uP402WwN8Y=
 github.com/lestrrat-go/blackmagic v1.0.0 h1:XzdxDbuQTz0RZZEmdU7cnQxUtFUzgCSPq8RCz4BxIi4=
 github.com/lestrrat-go/blackmagic v1.0.0/go.mod h1:TNgH//0vYSs8VXDCfkZLgIrVTTXQELZffUV0tz3MtdQ=
-github.com/lestrrat-go/httpcc v1.0.0 h1:FszVC6cKfDvBKcJv646+lkh4GydQg2Z29scgUfkOpYc=
-github.com/lestrrat-go/httpcc v1.0.0/go.mod h1:tGS/u00Vh5N6FHNkExqGGNId8e0Big+++0Gf8MBnAvE=
+github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
+github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
 github.com/lestrrat-go/iter v1.0.1 h1:q8faalr2dY6o8bV45uwrxq12bRa1ezKrB6oM9FUgN4A=
 github.com/lestrrat-go/iter v1.0.1/go.mod h1:zIdgO1mRKhn8l9vrZJZz9TUMMFbQbLeTsbqPDrJ/OJc=
-github.com/lestrrat-go/jwx v1.2.21 h1:n+yG95UMm5ZFsDdvsZmui+bqat4Cj/di4ys6XbgSlE8=
-github.com/lestrrat-go/jwx v1.2.21/go.mod h1:9cfxnOH7G1gN75CaJP2hKGcxFEx5sPh1abRIA/ZJVh4=
+github.com/lestrrat-go/jwx v1.2.22 h1:bCxMokwHNuJHVxgANP4OBddXGtQ9Oy+6cqp4O2rW7DU=
+github.com/lestrrat-go/jwx v1.2.22/go.mod h1:sAXjRwzSvCN6soO4RLoWWm1bVPpb8iOuv0IYfH8OWd8=
 github.com/lestrrat-go/option v1.0.0 h1:WqAWL8kh8VcSoD6xjSH34/1m8yxluXQbDeKNfvFeEO4=
 github.com/lestrrat-go/option v1.0.0/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
 github.com/lib/pq v0.0.0-20180327071824-d34b9ff171c2/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
@@ -1386,6 +1386,7 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN
 github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
 github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
 github.com/pquerna/cachecontrol v0.0.0-20171018203845-0dec1b30a021/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
+github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
 github.com/prometheus/client_golang v0.0.0-20180209125602-c332b6f63c06/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
 github.com/prometheus/client_golang v0.9.3/go.mod h1:/TN21ttK/J9q6uSwhBd54HahCDft0ttaMvbicHlPoso=
@@ -1660,12 +1661,12 @@ go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.2
 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.20.0/go.mod h1:2AboqHi0CiIZU0qwhtUfCYD1GeUzvvIXWNkhDt7ZMG4=
 go.opentelemetry.io/otel v0.20.0/go.mod h1:Y3ugLH2oa81t5QO+Lty+zXf8zC9L26ax4Nzoxm/dooo=
 go.opentelemetry.io/otel v1.6.0/go.mod h1:bfJD2DZVw0LBxghOTlgnlI0CV3hLDu9XF/QKOUXMTQQ=
-go.opentelemetry.io/otel v1.6.1 h1:6r1YrcTenBvYa1x491d0GGpTVBsNECmrc/K6b+zDeis=
-go.opentelemetry.io/otel v1.6.1/go.mod h1:blzUabWHkX6LJewxvadmzafgh/wnvBSDBdOuwkAtrWQ=
+go.opentelemetry.io/otel v1.6.3 h1:FLOfo8f9JzFVFVyU+MSRJc2HdEAXQgm7pIv2uFKRSZE=
+go.opentelemetry.io/otel v1.6.3/go.mod h1:7BgNga5fNlF/iZjG06hM3yofffp0ofKCDwSXx1GC4dI=
 go.opentelemetry.io/otel/bridge/opencensus v0.28.0 h1:FW1D3WQYwUVQHQkMUcMucI4A7hqlXaF0Yc2MYdsqBRA=
 go.opentelemetry.io/otel/bridge/opencensus v0.28.0/go.mod h1:DiAeRr5mifJcz09RCNAeF/ab1wTu7rlTljXcFG2LdB0=
-go.opentelemetry.io/otel/exporters/jaeger v1.6.1 h1:7xuwXr3qUWq48Chyuq+VvomV3KjXZLd5seQwg83s/sU=
-go.opentelemetry.io/otel/exporters/jaeger v1.6.1/go.mod h1:Cu6mKJ+LLTPuOBX830xM4wVKIsVpHSXa50uN7aAxraQ=
+go.opentelemetry.io/otel/exporters/jaeger v1.6.3 h1:7tvBU1Ydbzq080efuepYYqC1Pv3/vOFBgCSrxLb24d0=
+go.opentelemetry.io/otel/exporters/jaeger v1.6.3/go.mod h1:YgX3eZWbJzgrNyNHCK0otGreAMBTIAcObtZS2VRi6sU=
 go.opentelemetry.io/otel/exporters/otlp v0.20.0/go.mod h1:YIieizyaN77rtLJra0buKiNBOm9XQfkPEKBeuhoMwAM=
 go.opentelemetry.io/otel/metric v0.20.0/go.mod h1:598I5tYlH1vzBjn+BTuhzTCSb/9debfNp6R3s7Pr1eU=
 go.opentelemetry.io/otel/metric v0.28.0 h1:o5YNh+jxACMODoAo1bI7OES0RUW4jAMae0Vgs2etWAQ=
@@ -1673,16 +1674,16 @@ go.opentelemetry.io/otel/metric v0.28.0/go.mod h1:TrzsfQAmQaB1PDcdhBauLMk7nyyg9h
 go.opentelemetry.io/otel/oteltest v0.20.0/go.mod h1:L7bgKf9ZB7qCwT9Up7i9/pn0PWIa9FqQ2IQ8LoxiGnw=
 go.opentelemetry.io/otel/sdk v0.20.0/go.mod h1:g/IcepuwNsoiX5Byy2nNV0ySUF1em498m7hBWC279Yc=
 go.opentelemetry.io/otel/sdk v1.6.0/go.mod h1:PjLRUfDsoPy0zl7yrDGSUqjj43tL7rEtFdCEiGlxXRM=
-go.opentelemetry.io/otel/sdk v1.6.1 h1:ZmcNyMhcuAYIb/Nr6QhBPTMopMTbov/47wHt1gibkoY=
-go.opentelemetry.io/otel/sdk v1.6.1/go.mod h1:IVYrddmFZ+eJqu2k38qD3WezFR2pymCzm8tdxyh3R4E=
+go.opentelemetry.io/otel/sdk v1.6.3 h1:prSHYdwCQOX5DrsEzxowH3nLhoAzEBdZhvrR79scfLs=
+go.opentelemetry.io/otel/sdk v1.6.3/go.mod h1:A4iWF7HTXa+GWL/AaqESz28VuSBIcZ+0CV+IzJ5NMiQ=
 go.opentelemetry.io/otel/sdk/export/metric v0.20.0/go.mod h1:h7RBNMsDJ5pmI1zExLi+bJK+Dr8NQCh0qGhm1KDnNlE=
 go.opentelemetry.io/otel/sdk/metric v0.20.0/go.mod h1:knxiS8Xd4E/N+ZqKmUPf3gTTZ4/0TjTXukfxjzSTpHE=
 go.opentelemetry.io/otel/sdk/metric v0.28.0 h1:+1ndwHSiknwZtC8VmXM3xtMsd6kbFxtqti4qevn2J+o=
 go.opentelemetry.io/otel/sdk/metric v0.28.0/go.mod h1:DqJmT0ovBgoW6TJ8CAQyTnwxZPIp3KWtCiDDZ1uHAzU=
 go.opentelemetry.io/otel/trace v0.20.0/go.mod h1:6GjCW8zgDjwGHGa6GkyeB8+/5vjT16gUEi0Nf1iBdgw=
 go.opentelemetry.io/otel/trace v1.6.0/go.mod h1:qs7BrU5cZ8dXQHBGxHMOxwME/27YH2qEp4/+tZLLwJE=
-go.opentelemetry.io/otel/trace v1.6.1 h1:f8c93l5tboBYZna1nWk0W9DYyMzJXDWdZcJZ0Kb400U=
-go.opentelemetry.io/otel/trace v1.6.1/go.mod h1:RkFRM1m0puWIq10oxImnGEduNBzxiN7TXluRBtE+5j0=
+go.opentelemetry.io/otel/trace v1.6.3 h1:IqN4L+5b0mPNjdXIiZ90Ni4Bl5BRkDQywePLWemd9bc=
+go.opentelemetry.io/otel/trace v1.6.3/go.mod h1:GNJQusJlUgZl9/TQBPKU/Y/ty+0iVB5fjhKeJGZPGFs=
 go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI=
 go.starlark.net v0.0.0-20200306205701-8dd3e2ee1dd5/go.mod h1:nmDLcffg48OtT/PSW0Hg7FvpRQsQh5OSqIylirxKC7o=
 go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
@@ -1692,8 +1693,8 @@ go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=
 go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
 go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE=
 go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
-go.uber.org/automaxprocs v1.4.0 h1:CpDZl6aOlLhReez+8S3eEotD7Jx0Os++lemPlMULQP0=
-go.uber.org/automaxprocs v1.4.0/go.mod h1:/mTEdr7LvHhs0v7mjdxDreTz1OG5zdZGqgOnhWiR/+Q=
+go.uber.org/automaxprocs v1.5.1 h1:e1YG66Lrk73dn4qhg8WFSvhF0JuFQF0ERIp4rpuV8Qk=
+go.uber.org/automaxprocs v1.5.1/go.mod h1:BF4eumQw0P9GtnuxxovUd06vwm1o18oMzFtK66vU6XU=
 go.uber.org/config v1.4.0 h1:upnMPpMm6WlbZtXoasNkK4f0FhxwS+W4Iqz5oNznehQ=
 go.uber.org/config v1.4.0/go.mod h1:aCyrMHmUAc/s2h9sv1koP84M9ZF/4K+g2oleyESO/Ig=
 go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A=
@@ -2657,8 +2658,8 @@ modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
 modernc.org/ql v1.0.0/go.mod h1:xGVyrLIatPcO2C1JvI/Co8c0sr6y91HKFNy4pt9JXEY=
 modernc.org/sortutil v1.1.0/go.mod h1:ZyL98OQHJgH9IEfN71VsamvJgrtRX9Dj2gX+vH86L1k=
 modernc.org/sqlite v1.10.6/go.mod h1:Z9FEjUtZP4qFEg6/SiADg9XCER7aYy9a/j7Pg9P7CPs=
-modernc.org/sqlite v1.15.4 h1:pr3EA3Rety3j1c/9pCyGAe5d3vjF6wQwusHdgGCjIqc=
-modernc.org/sqlite v1.15.4/go.mod h1:Jwe13ItpESZ+78K5WS6+AjXsUg+JvirsjN3iIDO4C8k=
+modernc.org/sqlite v1.16.0 h1:DdvOGaWN0y+X7t2L7RUD63gcwbVjYZjcBZnA68g44EI=
+modernc.org/sqlite v1.16.0/go.mod h1:Jwe13ItpESZ+78K5WS6+AjXsUg+JvirsjN3iIDO4C8k=
 modernc.org/strutil v1.1.0/go.mod h1:lstksw84oURvj9y3tn8lGvRxyRC1S2+g5uuIzNfIOBs=
 modernc.org/strutil v1.1.1 h1:xv+J1BXY3Opl2ALrBwyfEikFAj8pmqcpnfmuwUwcozs=
 modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw=
diff --git a/internal/printer/colored/colored.go b/internal/printer/colored/colored.go
index 57470c7f..a0936a0c 100644
--- a/internal/printer/colored/colored.go
+++ b/internal/printer/colored/colored.go
@@ -16,10 +16,13 @@ var (
 	Principal               = color.New(color.FgCyan).SprintFunc()
 	REPLCmd                 = color.New(color.FgYellow).SprintFunc()
 	REPLError               = color.New(color.FgRed).SprintFunc()
+	REPLExpr                = color.New(color.FgCyan).SprintFunc()
+	REPLPolicyName          = color.New(color.FgCyan).SprintFunc()
+	REPLRule                = color.New(color.FgCyan, color.Bold).SprintFunc()
+	REPLSuccess             = color.New(color.FgGreen).SprintFunc()
 	REPLVar                 = color.New(color.FgCyan).SprintFunc()
 	Resource                = color.New(color.FgBlue).SprintFunc()
 	SkippedTest             = color.New(color.FgHiWhite).SprintFunc()
-	SuccessfulTest          = color.New(color.FgGreen).SprintFunc()
 	Suite                   = color.New(color.FgBlue, color.Bold).SprintFunc()
 	Trace                   = color.New(color.FgHiWhite).SprintFunc()
 	TraceComponentKey       = color.New(color.FgBlue).SprintFunc()
diff --git a/internal/printer/printer.go b/internal/printer/printer.go
index 156e0516..dddd6595 100644
--- a/internal/printer/printer.go
+++ b/internal/printer/printer.go
@@ -8,17 +8,20 @@ import (
 	"encoding/json"
 	"fmt"
 	"io"
+	"strings"
 
 	"github.com/alecthomas/chroma"
 	"github.com/alecthomas/chroma/formatters"
 	"github.com/alecthomas/chroma/lexers"
 	"github.com/alecthomas/chroma/styles"
+	"github.com/ghodss/yaml"
+	"google.golang.org/protobuf/encoding/protojson"
+	"google.golang.org/protobuf/proto"
+
 	effectv1 "github.com/cerbos/cerbos/api/genpb/cerbos/effect/v1"
 	enginev1 "github.com/cerbos/cerbos/api/genpb/cerbos/engine/v1"
 	"github.com/cerbos/cerbos/internal/outputcolor"
 	"github.com/cerbos/cerbos/internal/printer/colored"
-	"google.golang.org/protobuf/encoding/protojson"
-	"google.golang.org/protobuf/proto"
 )
 
 func New(stdout, stderr io.Writer) *Printer {
@@ -85,6 +88,63 @@ func (p *Printer) PrintJSON(val any, colorLevel outputcolor.Level) error {
 	return nil
 }
 
+func (p *Printer) coloredYAML(data string, colorLevel outputcolor.Level) ([]byte, error) {
+	lexer := chroma.Coalesce(lexers.Get("yaml"))
+	if lexer == nil {
+		lexer = lexers.Fallback
+	}
+
+	var formatter chroma.Formatter
+	switch colorLevel {
+	case outputcolor.Basic:
+		formatter = formatters.TTY
+	case outputcolor.Ansi256:
+		formatter = formatters.TTY256
+	case outputcolor.Ansi16m:
+		formatter = formatters.TTY16m
+	default:
+		formatter = formatters.NoOp
+	}
+
+	iterator, err := lexer.Tokenise(nil, data)
+	if err != nil {
+		return nil, fmt.Errorf("failed to tokenise YAML: %w", err)
+	}
+
+	var yml bytes.Buffer
+	err = formatter.Format(&yml, styles.SolarizedDark256, iterator)
+	if err != nil {
+		return nil, fmt.Errorf("failed to format yaml: %w", err)
+	}
+
+	return yml.Bytes(), nil
+}
+
+func (p *Printer) PrintProtoYAML(message proto.Message, colorLevel outputcolor.Level, indent int) error {
+	data, err := protojson.MarshalOptions{Multiline: true}.Marshal(message)
+	if err != nil {
+		return fmt.Errorf("failed to encode JSON: %w", err)
+	}
+
+	yamlBytes, err := yaml.JSONToYAML(data)
+	if err != nil {
+		return fmt.Errorf("failed to convert data to YAML: %w", err)
+	}
+
+	if colorLevel.Enabled() {
+		yamlBytes, err = p.coloredYAML(string(yamlBytes), colorLevel)
+		if err != nil {
+			return err
+		}
+	}
+
+	for _, line := range strings.Split(strings.TrimSuffix(string(yamlBytes), "\n"), "\n") {
+		fmt.Fprintf(p.stdout, "%s%s\n", strings.Repeat("  ", indent), line)
+	}
+
+	return nil
+}
+
 func (p *Printer) PrintProtoJSON(message proto.Message, colorLevel outputcolor.Level) error {
 	data, err := protojson.MarshalOptions{Multiline: true}.Marshal(message)
 	if err != nil {
diff --git a/tools/go.mod b/tools/go.mod
index 64d16280..a529a9b7 100644
--- a/tools/go.mod
+++ b/tools/go.mod
@@ -3,7 +3,7 @@ module github.com/cerbos/cerbos/tools
 go 1.18
 
 require (
-	github.com/bojand/ghz v0.106.1
+	github.com/bojand/ghz v0.108.0
 	github.com/bufbuild/buf v1.3.1
 	github.com/cerbos/protoc-gen-go-hashpb v0.1.0
 	github.com/envoyproxy/protoc-gen-validate v0.6.7
@@ -13,11 +13,11 @@ require (
 	github.com/goreleaser/goreleaser v1.7.0
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.0
 	github.com/planetscale/vtprotobuf v0.2.0
-	github.com/vektra/mockery/v2 v2.10.2
+	github.com/vektra/mockery/v2 v2.10.4
 	go.elastic.co/go-licence-detector v0.5.0
 	google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0
 	google.golang.org/protobuf v1.28.0
-	gotest.tools/gotestsum v1.7.0
+	gotest.tools/gotestsum v1.8.0
 )
 
 require (
diff --git a/tools/go.sum b/tools/go.sum
index e6210f9e..e3c2544b 100644
--- a/tools/go.sum
+++ b/tools/go.sum
@@ -295,8 +295,8 @@ github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb h1:m935MPodAbYS46DG4
 github.com/blakesmith/ar v0.0.0-20190502131153-809d4375e1fb/go.mod h1:PkYb9DJNAwrSvRx5DYA+gUcOIgTGVMNkfSCbZM8cWpI=
 github.com/blizzy78/varnamelen v0.7.0 h1:Y7LzPAysnU3YAabhqXkP9/G9/r7Ks9fUEvOjXoXStlY=
 github.com/blizzy78/varnamelen v0.7.0/go.mod h1:V9TzQZ4fLJ1DSrjVDfl89H7aMnTvKkApdHeyESmyR7k=
-github.com/bojand/ghz v0.106.1 h1:u9RmtTVuPqk1Pg5s3dw3XxkNERmNDZRRpciTza4sOgU=
-github.com/bojand/ghz v0.106.1/go.mod h1:l3GXrJtJB3KbuhJ3Inr4Osu2fVwIEvJ5U69I2YA1h14=
+github.com/bojand/ghz v0.108.0 h1:rpLA8MKhF0FhrZx22ufTsMGBl4J99LJMKMl6RrW4ExM=
+github.com/bojand/ghz v0.108.0/go.mod h1:mAEf0dbMwX2vXQBIMPR0UUSlMyqJKvcH+lVJj2tYk/w=
 github.com/bojand/hri v1.1.0/go.mod h1:qwGosuHpNn1S0nyw/mExN0+WZrDf4bQyWjhWh51y3VY=
 github.com/bombsimon/wsl/v3 v3.1.0/go.mod h1:st10JtZYLE4D5sC7b8xV4zTKZwAQjCH/Hy2Pm1FNZIc=
 github.com/bombsimon/wsl/v3 v3.3.0 h1:Mka/+kRLoQJq7g2rggtgQsjuI/K5Efd87WX96EWFxjM=
@@ -1356,8 +1356,8 @@ github.com/valyala/fasthttp v1.15.1/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl
 github.com/valyala/fasttemplate v1.0.1/go.mod h1:UQGH1tvbgY+Nz5t2n7tXsz52dQxojPUpymEIMZ47gx8=
 github.com/valyala/quicktemplate v1.6.2/go.mod h1:mtEJpQtUiBV0SHhMX6RtiJtqxncgrfmjcUy5T68X8TM=
 github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
-github.com/vektra/mockery/v2 v2.10.2 h1:ISAFkB3rQS6Y3aDZzAKtDwgeyDknwNa1aBE3Zgx0h+I=
-github.com/vektra/mockery/v2 v2.10.2/go.mod h1:m/WO2UzWzqgVX3nvqpRQq70I4Z7jbSCRhdmkgtp+Ab4=
+github.com/vektra/mockery/v2 v2.10.4 h1:nMdsCKIS7ZdNTRNS/77Bx6Q/UbasGcfc3Nx7JO7HGTg=
+github.com/vektra/mockery/v2 v2.10.4/go.mod h1:m/WO2UzWzqgVX3nvqpRQq70I4Z7jbSCRhdmkgtp+Ab4=
 github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8/go.mod h1:dniwbG03GafCjFohMDmz6Zc6oCuiqgH6tGNyXTkHzXE=
 github.com/xanzy/go-gitlab v0.59.0 h1:fAr6rT/YIdfmBavYgI42+Op7yAAex2Y4xOfvbjN9hxQ=
 github.com/xanzy/go-gitlab v0.59.0/go.mod h1:F0QEXwmqiBUxCgJm8fE9S+1veX4XC9Z4cfaAbqwk4YM=
@@ -2101,8 +2101,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
 gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b h1:h8qDotaEPuJATrMmW04NCwg7v22aHH28wwpauUhK9Oo=
 gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-gotest.tools/gotestsum v1.7.0 h1:RwpqwwFKBAa2h+F6pMEGpE707Edld0etUD3GhqqhDNc=
-gotest.tools/gotestsum v1.7.0/go.mod h1:V1m4Jw3eBerhI/A6qCxUE07RnCg7ACkKj9BYcAm09V8=
+gotest.tools/gotestsum v1.8.0 h1:ltj5KWhbQjt1Yx+IPBj0Dk2HVDUs9OlLmGC2JvivTMo=
+gotest.tools/gotestsum v1.8.0/go.mod h1:ctqdxBSCPv80kAFjYvFNpPntBrE5HAQnLiOKBGLmOBs=
 gotest.tools/v3 v3.0.3 h1:4AuOwCGf4lLR9u3YOe2awrHygurzhO/HeQ6laiA6Sx0=
 gotest.tools/v3 v3.0.3/go.mod h1:Z7Lb0S5l+klDB31fvDQX8ss/FlKDxtlFlw3Oa8Ymbl8=
 honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=


Test patch:
diff --git a/.github/workflows/e2e.yaml b/.github/workflows/e2e.yaml
index cb733654..34bb4f10 100644
--- a/.github/workflows/e2e.yaml
+++ b/.github/workflows/e2e.yaml
@@ -10,7 +10,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Install Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: 1.18.x
 
diff --git a/.github/workflows/pr-test.yaml b/.github/workflows/pr-test.yaml
index 2de5aada..5b7dde34 100644
--- a/.github/workflows/pr-test.yaml
+++ b/.github/workflows/pr-test.yaml
@@ -30,7 +30,7 @@ jobs:
     runs-on: ubuntu-latest
     steps:
       - name: Install Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: 1.18.x
 
@@ -65,7 +65,7 @@ jobs:
           CERBOS_DEBUG_ENGINE: "true"
 
       - name: Upload to CodeCov
-        uses: codecov/codecov-action@v2
+        uses: codecov/codecov-action@v3
         with:
           token: ${{ secrets.CODECOV_TOKEN }}
           files: "unit.cover,integration.cover"
@@ -93,7 +93,7 @@ jobs:
     steps:
       - uses: actions/checkout@v3
       - name: Install Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: 1.18.x
       - name: golangci-lint
diff --git a/.github/workflows/test-matrix.yaml b/.github/workflows/test-matrix.yaml
index 8b8ce6c1..5bc39d65 100644
--- a/.github/workflows/test-matrix.yaml
+++ b/.github/workflows/test-matrix.yaml
@@ -12,7 +12,7 @@ jobs:
         os: [ubuntu-latest, macos-latest, windows-latest]
     steps:
       - name: Install Go
-        uses: actions/setup-go@v2
+        uses: actions/setup-go@v3
         with:
           go-version: 1.18.x
 
diff --git a/cmd/cerbos/repl/internal/repl_test.go b/cmd/cerbos/repl/internal/repl_test.go
index 0e58a19f..74f1bd2b 100644
--- a/cmd/cerbos/repl/internal/repl_test.go
+++ b/cmd/cerbos/repl/internal/repl_test.go
@@ -4,15 +4,24 @@
 package internal
 
 import (
+	"fmt"
+	"path/filepath"
 	"testing"
 
-	enginev1 "github.com/cerbos/cerbos/api/genpb/cerbos/engine/v1"
-	"github.com/cerbos/cerbos/internal/conditions"
 	"github.com/google/cel-go/common/types/ref"
 	"github.com/google/go-cmp/cmp"
 	"github.com/stretchr/testify/require"
+	"google.golang.org/protobuf/encoding/protojson"
+	"google.golang.org/protobuf/proto"
 	"google.golang.org/protobuf/testing/protocmp"
 	"google.golang.org/protobuf/types/known/structpb"
+
+	enginev1 "github.com/cerbos/cerbos/api/genpb/cerbos/engine/v1"
+	policyv1 "github.com/cerbos/cerbos/api/genpb/cerbos/policy/v1"
+	runtimev1 "github.com/cerbos/cerbos/api/genpb/cerbos/runtime/v1"
+	"github.com/cerbos/cerbos/internal/compile"
+	"github.com/cerbos/cerbos/internal/conditions"
+	"github.com/cerbos/cerbos/internal/test"
 )
 
 type DirectiveTest struct {
@@ -23,6 +32,12 @@ type DirectiveTest struct {
 
 func TestREPL(t *testing.T) {
 	toRefVal := conditions.StdEnv.TypeAdapter().NativeToValue
+	drPath := filepath.Join(test.PathToDir(t, "store"), "derived_roles", "derived_roles_01.yaml")
+	rpPath := filepath.Join(test.PathToDir(t, "store"), "resource_policies", "policy_01.yaml")
+	ppPath := filepath.Join(test.PathToDir(t, "store"), "principal_policies", "policy_01.yaml")
+	drConds := loadConditionsFromPolicy(t, drPath)
+	rpConds := loadConditionsFromPolicy(t, rpPath)
+	ppConds := loadConditionsFromPolicy(t, ppPath)
 
 	testCases := []struct {
 		name       string
@@ -258,6 +273,62 @@ func TestREPL(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "load_derived_roles",
+			directives: []DirectiveTest{
+				{
+					Directive: fmt.Sprintf(":load %s", drPath),
+					Check: func(t *testing.T, output *mockOutput) {
+						t.Helper()
+						for idx, r := range output.rules {
+							rd, ok := r.(*policyv1.RoleDef)
+							require.True(t, ok)
+							condition, err := compile.Condition(rd.Condition)
+							require.NoError(t, err)
+							require.JSONEq(t, protojson.Format(drConds[idx]), protojson.Format(condition))
+						}
+					},
+				},
+			},
+		},
+		{
+			name: "load_resource_policy",
+			directives: []DirectiveTest{
+				{
+					Directive: fmt.Sprintf(":load %s", rpPath),
+					Check: func(t *testing.T, output *mockOutput) {
+						t.Helper()
+						for idx, r := range output.rules {
+							rr, ok := r.(*policyv1.ResourceRule)
+							require.True(t, ok)
+							condition, err := compile.Condition(rr.Condition)
+							require.NoError(t, err)
+							require.JSONEq(t, protojson.Format(rpConds[idx]), protojson.Format(condition))
+						}
+					},
+				},
+			},
+		},
+		{
+			name: "load_principal_policy",
+			directives: []DirectiveTest{
+				{
+					Directive: fmt.Sprintf(":load %s", ppPath),
+					Check: func(t *testing.T, output *mockOutput) {
+						t.Helper()
+						for idx, r := range output.rules {
+							pr, ok := r.(*policyv1.PrincipalRule)
+							for _, action := range pr.Actions {
+								require.True(t, ok)
+								condition, err := compile.Condition(action.Condition)
+								require.NoError(t, err)
+								require.JSONEq(t, protojson.Format(ppConds[idx]), protojson.Format(condition))
+							}
+						}
+					},
+				},
+			},
+		},
 	}
 
 	for _, tc := range testCases {
@@ -287,8 +358,10 @@ type mockOutput struct {
 	msg        string
 	args       []any
 	jsonObj    any
+	yamlObj    any
 	resultName string
 	resultVal  ref.Val
+	rules      []any
 	err        error
 }
 
@@ -306,11 +379,58 @@ func (mo *mockOutput) PrintResult(name string, val ref.Val) {
 	mo.resultVal = val
 }
 
+func (mo *mockOutput) PrintRule(_ int, rule any) error {
+	mo.rules = append(mo.rules, rule)
+	return nil
+}
+
 func (mo *mockOutput) PrintJSON(obj any) {
 	mo.jsonObj = obj
 }
 
+func (mo *mockOutput) PrintYAML(obj proto.Message, _ int) {
+	mo.yamlObj = obj
+}
+
 func (mo *mockOutput) PrintErr(msg string, err error) {
 	mo.msg = msg
 	mo.err = err
 }
+
+func loadConditionsFromPolicy(t *testing.T, path string) []*runtimev1.Condition {
+	t.Helper()
+
+	p := test.LoadPolicy(t, path)
+
+	var conds []*runtimev1.Condition
+	switch pt := p.PolicyType.(type) {
+	case *policyv1.Policy_ResourcePolicy:
+		for _, rule := range pt.ResourcePolicy.Rules {
+			if rule.Condition != nil {
+				cond, err := compile.Condition(rule.Condition)
+				require.NoError(t, err)
+				conds = append(conds, cond)
+			}
+		}
+	case *policyv1.Policy_DerivedRoles:
+		for _, def := range pt.DerivedRoles.Definitions {
+			if def.Condition != nil {
+				cond, err := compile.Condition(def.Condition)
+				require.NoError(t, err)
+				conds = append(conds, cond)
+			}
+		}
+	case *policyv1.Policy_PrincipalPolicy:
+		for _, rule := range pt.PrincipalPolicy.Rules {
+			for _, action := range rule.Actions {
+				if action.Condition != nil {
+					cond, err := compile.Condition(action.Condition)
+					require.NoError(t, err)
+					conds = append(conds, cond)
+				}
+			}
+		}
+	}
+
+	return conds
+}
diff --git a/internal/test/mocks/Index.go b/internal/test/mocks/Index.go
index 7bbd1238..80f607ef 100644
--- a/internal/test/mocks/Index.go
+++ b/internal/test/mocks/Index.go
@@ -1,7 +1,7 @@
 // Copyright 2021-2022 Zenauth Ltd.
 // SPDX-License-Identifier: Apache-2.0
 
-// Code generated by mockery v2.10.2. DO NOT EDIT.
+// Code generated by mockery v2.10.4. DO NOT EDIT.
 
 package mocks
 
diff --git a/internal/test/mocks/Store.go b/internal/test/mocks/Store.go
index 292b8ef5..e58ad1d6 100644
--- a/internal/test/mocks/Store.go
+++ b/internal/test/mocks/Store.go
@@ -1,7 +1,7 @@
 // Copyright 2021-2022 Zenauth Ltd.
 // SPDX-License-Identifier: Apache-2.0
 
-// Code generated by mockery v2.10.2. DO NOT EDIT.
+// Code generated by mockery v2.10.4. DO NOT EDIT.
 
 package mocks
 
diff --git a/internal/test/testdata/store/derived_roles/derived_roles_01.yaml b/internal/test/testdata/store/derived_roles/derived_roles_01.yaml
index 94395e12..dc75ea36 100644
--- a/internal/test/testdata/store/derived_roles/derived_roles_01.yaml
+++ b/internal/test/testdata/store/derived_roles/derived_roles_01.yaml
@@ -13,4 +13,4 @@ derivedRoles:
       parentRoles: ["employee"]
       condition:
         match:
-          expr: R.attr.owner == P.id
+          expr: R.attr.owner == P.id
\ No newline at end of file