Repository: cerbos/cerbos
Pull request #852 (2022-04-18 10:11:37)

chore(deps): Bump github.com/vektra/mockery/v2 from 2.10.4 to 2.10.6 in /tools
Bumps [github.com/vektra/mockery/v2](https://github.com/vektra/mockery) from 2.10.4 to 2.10.6.
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/vektra/mockery/releases">github.com/vektra/mockery/v2's releases</a>.</em></p>
<blockquote>
<h2>v2.10.6</h2>
<h2>Changelog</h2>
<ul>
<li>df6e689 Add PR/issue templates</li>
<li>e8bf201 Add golang-1.18 note</li>
<li>54589be Merge pull request <a href="https://github-redirect.dependabot.com/vektra/mockery/issues/445">#445</a> from bigbluedisco/fix/bump-golang-org-x-tools</li>
<li>aa25af0 fix: bump golang.org/x/tools to v0.1.10 to fix some go 1.18 issues</li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/vektra/mockery/commit/54589be6dd37d7ae95959995575f11931cf864c8"><code>54589be</code></a> Merge pull request <a href="https://github-redirect.dependabot.com/vektra/mockery/issues/445">#445</a> from bigbluedisco/fix/bump-golang-org-x-tools</li>
<li><a href="https://github.com/vektra/mockery/commit/aa25af0c7dab9b25794455ba66ad18ab202a546e"><code>aa25af0</code></a> fix: bump golang.org/x/tools to v0.1.10 to fix some go 1.18 issues</li>
<li><a href="https://github.com/vektra/mockery/commit/e8bf20167598ee4ebc67435c8ee3896218a354e6"><code>e8bf201</code></a> Add golang-1.18 note</li>
<li><a href="https://github.com/vektra/mockery/commit/df6e689d948adb8eb0ee4301fe945795defcd2d7"><code>df6e689</code></a> Add PR/issue templates</li>
<li>See full diff in <a href="https://github.com/vektra/mockery/compare/v2.10.4...v2.10.6">compare view</a></li>
</ul>
</details>
<br />


[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=github.com/vektra/mockery/v2&package-manager=go_modules&previous-version=2.10.4&new-version=2.10.6)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores)

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself)
- `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)


</details>

codecov-commenter: # [Codecov](https://codecov.io/gh/cerbos/cerbos/pull/852?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos) Report
> Merging [#852](https://codecov.io/gh/cerbos/cerbos/pull/852?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos) (43ba074) into [main](https://codecov.io/gh/cerbos/cerbos/commit/46d1f97b72f93a2d47fdb931bf69c3d0cf18a6fa?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos) (46d1f97) will **increase** coverage by `0.20%`.
> The diff coverage is `n/a`.

[![Impacted file tree graph](https://codecov.io/gh/cerbos/cerbos/pull/852/graphs/tree.svg?width=650&height=150&src=pr&token=tGaxiUZUzL&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos)](https://codecov.io/gh/cerbos/cerbos/pull/852?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos)

```diff
@@            Coverage Diff             @@
##             main     #852      +/-   ##
==========================================
+ Coverage   53.97%   54.17%   +0.20%     
==========================================
  Files         101      101              
  Lines       10969    10969              
==========================================
+ Hits         5920     5943      +23     
+ Misses       4538     4517      -21     
+ Partials      511      509       -2     
```


| [Impacted Files](https://codecov.io/gh/cerbos/cerbos/pull/852?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos) | Coverage Δ | |
|---|---|---|
| [internal/engine/planner.go](https://codecov.io/gh/cerbos/cerbos/pull/852/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos#diff-aW50ZXJuYWwvZW5naW5lL3BsYW5uZXIuZ28=) | `78.32% <0.00%> (-0.58%)` | :arrow_down: |
| [internal/engine/engine.go](https://codecov.io/gh/cerbos/cerbos/pull/852/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos#diff-aW50ZXJuYWwvZW5naW5lL2VuZ2luZS5nbw==) | `69.97% <0.00%> (+0.80%)` | :arrow_up: |
| [internal/server/tests.go](https://codecov.io/gh/cerbos/cerbos/pull/852/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos#diff-aW50ZXJuYWwvc2VydmVyL3Rlc3RzLmdv) | `95.13% <0.00%> (+0.91%)` | :arrow_up: |
| [internal/storage/index/builder.go](https://codecov.io/gh/cerbos/cerbos/pull/852/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos#diff-aW50ZXJuYWwvc3RvcmFnZS9pbmRleC9idWlsZGVyLmdv) | `84.35% <0.00%> (+5.02%)` | :arrow_up: |
| [internal/compile/errors.go](https://codecov.io/gh/cerbos/cerbos/pull/852/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=cerbos#diff-aW50ZXJuYWwvY29tcGlsZS9lcnJvcnMuZ28=) | `55.55% <0.00%> (+15.87%)` | :arrow_up: |


Patch:
diff --git a/cmd/cerbos/repl/internal/repl.go b/cmd/cerbos/repl/internal/repl.go
index 97e8e2f7..28fe02fb 100644
--- a/cmd/cerbos/repl/internal/repl.go
+++ b/cmd/cerbos/repl/internal/repl.go
@@ -61,15 +61,21 @@ const (
 	yamlIndent      = 2
 )
 
+type policyHolder struct {
+	key       string
+	variables map[string]string
+	rules     []proto.Message
+}
+
 type REPL struct {
-	policyKey string
-	output    Output
-	vars      variables
-	decls     map[string]*exprpb.Decl
-	reader    *liner.State
-	parser    *participle.Parser
-	toRefVal  func(any) ref.Val
-	rules     []any
+	output   Output
+	vars     variables
+	decls    map[string]*exprpb.Decl
+	reader   *liner.State
+	parser   *participle.Parser
+	toRefVal func(any) ref.Val
+	policy   *policyHolder
+	varV     map[string]any
 }
 
 func NewREPL(reader *liner.State, output Output) (*REPL, error) {
@@ -188,6 +194,8 @@ func (r *REPL) processDirective(line string) error {
 
 func (r *REPL) reset() error {
 	r.vars, r.decls = resetVarsAndDecls()
+	r.policy = nil
+	r.varV = nil
 	r.output.Println()
 
 	return nil
@@ -216,15 +224,14 @@ func (r *REPL) showVars() error {
 }
 
 func (r *REPL) showRules() error {
-	if r.rules == nil {
+	if r.policy == 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 {
+	r.output.Print(fmt.Sprintf("Conditional rules in '%s'\n", colored.REPLPolicyName(r.policy.key)))
+	for idx, rule := range r.policy.rules {
+		if err := r.output.PrintRule(idx, rule); err != nil {
 			return err
 		}
 	}
@@ -250,6 +257,8 @@ func (r *REPL) setSpecialVar(name, value string) error {
 		r.vars[conditions.CELPrincipalAbbrev] = r.toRefVal(request.Principal)
 		r.vars[conditions.CELResourceAbbrev] = r.toRefVal(request.Resource)
 
+		r.evalPolicyVariables()
+
 	case conditions.CELPrincipalAbbrev, qualifiedPrincipal:
 		request, err := getCheckInput(r.vars)
 		if err != nil {
@@ -270,6 +279,8 @@ func (r *REPL) setSpecialVar(name, value string) error {
 		r.vars[conditions.CELPrincipalAbbrev] = principalVal
 		r.vars[conditions.CELResourceAbbrev] = r.toRefVal(request.Resource)
 
+		r.evalPolicyVariables()
+
 	case conditions.CELResourceAbbrev, qualifiedResource:
 		request, err := getCheckInput(r.vars)
 		if err != nil {
@@ -290,17 +301,17 @@ func (r *REPL) setSpecialVar(name, value string) error {
 		r.vars[conditions.CELPrincipalAbbrev] = r.toRefVal(request.Principal)
 		r.vars[conditions.CELResourceAbbrev] = resourceVal
 
+		r.evalPolicyVariables()
+
 	case conditions.CELVariablesIdent, conditions.CELVariablesAbbrev:
 		var v map[string]any
 		if err := json.Unmarshal([]byte(value), &v); err != nil {
 			return fmt.Errorf("failed to unmarshal JSON as %q: %w", name, err)
 		}
 
-		varsVal := r.toRefVal(v)
-		r.output.PrintResult(name, varsVal)
-
-		r.vars[conditions.CELVariablesIdent] = varsVal
-		r.vars[conditions.CELVariablesAbbrev] = varsVal
+		r.addToVarV(v)
+		r.evalPolicyVariables()
+		r.output.PrintResult(name, r.vars[conditions.CELVariablesIdent])
 
 	default:
 		return fmt.Errorf("setting %q is unsupported", name)
@@ -309,6 +320,20 @@ func (r *REPL) setSpecialVar(name, value string) error {
 	return nil
 }
 
+func (r *REPL) addToVarV(v map[string]any) {
+	if r.varV == nil {
+		r.varV = make(map[string]any, len(v))
+	}
+
+	for name, val := range v {
+		r.varV[name] = val
+	}
+
+	varsVal := r.toRefVal(r.varV)
+	r.vars[conditions.CELVariablesIdent] = varsVal
+	r.vars[conditions.CELVariablesAbbrev] = varsVal
+}
+
 func (r *REPL) processExpr(name, expr string) error {
 	val, tpe, err := r.evalExpr(expr)
 	if err != nil {
@@ -353,7 +378,7 @@ func (r *REPL) evalExpr(expr string) (ref.Val, *exprpb.Type, error) {
 }
 
 func (r *REPL) loadRulesFromPolicy(path string) error {
-	f, err := os.Open(strings.Trim(path, " "))
+	f, err := os.Open(strings.TrimSpace(path))
 	if err != nil {
 		return fmt.Errorf("failed to open policy file at %s: %w", path, err)
 	}
@@ -364,33 +389,20 @@ func (r *REPL) loadRulesFromPolicy(path string) error {
 		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
+	ph := &policyHolder{key: namer.PolicyKey(p), variables: p.Variables}
 	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)
+				ph.rules = append(ph.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)
+				ph.rules = append(ph.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 {
@@ -401,27 +413,47 @@ func (r *REPL) loadRulesFromPolicy(path string) error {
 							action,
 						},
 					}
-					r.rules = append(r.rules, pr)
+					ph.rules = append(ph.rules, pr)
 				}
 			}
 		}
-
-		r.policyKey = namer.PolicyKey(p)
-		r.output.Println(fmt.Sprintf("Principal policy '%s' loaded", colored.REPLPolicyName(namer.PolicyKey(p))))
 	}
 
+	r.policy = ph
+	r.evalPolicyVariables()
+
+	r.output.Println(fmt.Sprintf("Loaded %s", colored.REPLPolicyName(ph.key)))
 	r.output.Println()
-	err = r.showRules()
-	if err != nil {
-		return fmt.Errorf("failed to display rules after loading a policy: %w", err)
+
+	if len(ph.variables) > 0 {
+		r.output.Println("Policy variables:")
+		r.output.PrintJSON(ph.variables)
 	}
 
-	return nil
+	return r.showRules()
+}
+
+func (r *REPL) evalPolicyVariables() {
+	if r.policy == nil || len(r.policy.variables) == 0 {
+		return
+	}
+
+	result := make(map[string]any, len(r.policy.variables))
+	for name, expr := range r.policy.variables {
+		v, _, err := r.evalExpr(expr)
+		if err != nil {
+			result[name] = types.NewErr("failed to evaluate '%s = %s': %v", name, expr, err)
+		} else {
+			result[name] = v
+		}
+	}
+
+	r.addToVarV(result)
 }
 
 func (r *REPL) execRule(id int) error {
-	if id >= len(r.rules) {
-		return fmt.Errorf("failed to find rule with id %d", id)
+	if r.policy == nil || id >= len(r.policy.rules) {
+		return fmt.Errorf("failed to find rule %d", id)
 	}
 
 	return r.evalCondition(id)
@@ -429,7 +461,7 @@ func (r *REPL) execRule(id int) error {
 
 func (r *REPL) evalCondition(id int) error {
 	var cond *policyv1.Condition
-	switch rt := r.rules[id].(type) {
+	switch rt := r.policy.rules[id].(type) {
 	case *policyv1.ResourceRule:
 		cond = rt.Condition
 	case *policyv1.RoleDef:
@@ -446,12 +478,7 @@ func (r *REPL) evalCondition(id int) error {
 	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
+	return pterm.DefaultTree.WithRoot(pterm.NewTreeFromLeveledList(eo.tree)).Render()
 }
 
 func (r *REPL) doEvalCondition(condition *runtimev1.Condition) *eval {
@@ -523,7 +550,7 @@ type Output interface {
 	Print(string, ...any)
 	Println(...any)
 	PrintResult(string, ref.Val)
-	PrintRule(int, any) error
+	PrintRule(int, proto.Message) error
 	PrintJSON(any)
 	PrintYAML(proto.Message, int)
 	PrintErr(string, error)
@@ -546,14 +573,9 @@ 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))
-	}
-
+func (po *PrinterOutput) PrintRule(id int, rule proto.Message) error {
 	po.Println(fmt.Sprintf("[%s]", colored.REPLRule(fmt.Sprintf("%s%d", rulePrefix, id))))
-	po.PrintYAML(msg, yamlIndent)
+	po.PrintYAML(rule, yamlIndent)
 
 	return nil
 }
@@ -599,7 +621,7 @@ func (po *PrinterOutput) PrintJSON(obj any) {
 }
 
 func (po *PrinterOutput) PrintYAML(obj proto.Message, indent int) {
-	if err := po.Printer.PrintProtoYAML(obj, po.level, indent); err != nil {
+	if err := po.PrintProtoYAML(obj, po.level, indent); err != nil {
 		po.Println("<...>")
 	}
 	po.Println()
diff --git a/tools/go.mod b/tools/go.mod
index 9151b605..2d7889fe 100644
--- a/tools/go.mod
+++ b/tools/go.mod
@@ -13,7 +13,7 @@ require (
 	github.com/goreleaser/goreleaser v1.8.2
 	github.com/grpc-ecosystem/grpc-gateway/v2 v2.10.0
 	github.com/planetscale/vtprotobuf v0.3.0
-	github.com/vektra/mockery/v2 v2.10.4
+	github.com/vektra/mockery/v2 v2.10.6
 	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
diff --git a/tools/go.sum b/tools/go.sum
index 24aae203..0dc2d7bc 100644
--- a/tools/go.sum
+++ b/tools/go.sum
@@ -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.4 h1:nMdsCKIS7ZdNTRNS/77Bx6Q/UbasGcfc3Nx7JO7HGTg=
-github.com/vektra/mockery/v2 v2.10.4/go.mod h1:m/WO2UzWzqgVX3nvqpRQq70I4Z7jbSCRhdmkgtp+Ab4=
+github.com/vektra/mockery/v2 v2.10.6 h1:iLVqC9FozavYx27ZwfXipuizLBN8YzXlh9x5fufk48w=
+github.com/vektra/mockery/v2 v2.10.6/go.mod h1:8vf4KDDUptfkyypzdHLuE7OE2xA7Gdt60WgIS8PgD+U=
 github.com/viki-org/dnscache v0.0.0-20130720023526-c70c1f23c5d8/go.mod h1:dniwbG03GafCjFohMDmz6Zc6oCuiqgH6tGNyXTkHzXE=
 github.com/xanzy/go-gitlab v0.63.0 h1:a9fXpKWykUS6dowapFej/2Wjf4aOAEFC1q2ZIcz4IpI=
 github.com/xanzy/go-gitlab v0.63.0/go.mod h1:F0QEXwmqiBUxCgJm8fE9S+1veX4XC9Z4cfaAbqwk4YM=


Test patch:
diff --git a/cmd/cerbos/repl/internal/directive_test.go b/cmd/cerbos/repl/internal/directive_test.go
index ffe9c75a..c1c2ef40 100644
--- a/cmd/cerbos/repl/internal/directive_test.go
+++ b/cmd/cerbos/repl/internal/directive_test.go
@@ -24,6 +24,9 @@ func TestDirectiveParser(t *testing.T) {
 				require.False(t, rd.Reset)
 				require.False(t, rd.Vars)
 				require.False(t, rd.Help)
+				require.False(t, rd.Rules)
+				require.Nil(t, rd.Exec)
+				require.Nil(t, rd.Load)
 				require.Nil(t, rd.Let)
 			},
 		},
@@ -35,6 +38,9 @@ func TestDirectiveParser(t *testing.T) {
 				require.False(t, rd.Reset)
 				require.False(t, rd.Vars)
 				require.False(t, rd.Help)
+				require.False(t, rd.Rules)
+				require.Nil(t, rd.Exec)
+				require.Nil(t, rd.Load)
 				require.Nil(t, rd.Let)
 			},
 		},
@@ -46,6 +52,9 @@ func TestDirectiveParser(t *testing.T) {
 				require.False(t, rd.Reset)
 				require.False(t, rd.Vars)
 				require.False(t, rd.Help)
+				require.False(t, rd.Rules)
+				require.Nil(t, rd.Exec)
+				require.Nil(t, rd.Load)
 				require.Nil(t, rd.Let)
 			},
 		},
@@ -57,6 +66,9 @@ func TestDirectiveParser(t *testing.T) {
 				require.True(t, rd.Reset)
 				require.False(t, rd.Vars)
 				require.False(t, rd.Help)
+				require.False(t, rd.Rules)
+				require.Nil(t, rd.Exec)
+				require.Nil(t, rd.Load)
 				require.Nil(t, rd.Let)
 			},
 		},
@@ -68,6 +80,9 @@ func TestDirectiveParser(t *testing.T) {
 				require.False(t, rd.Reset)
 				require.True(t, rd.Vars)
 				require.False(t, rd.Help)
+				require.False(t, rd.Rules)
+				require.Nil(t, rd.Exec)
+				require.Nil(t, rd.Load)
 				require.Nil(t, rd.Let)
 			},
 		},
@@ -79,6 +94,9 @@ func TestDirectiveParser(t *testing.T) {
 				require.False(t, rd.Reset)
 				require.False(t, rd.Vars)
 				require.True(t, rd.Help)
+				require.False(t, rd.Rules)
+				require.Nil(t, rd.Exec)
+				require.Nil(t, rd.Load)
 				require.Nil(t, rd.Let)
 			},
 		},
@@ -90,6 +108,23 @@ func TestDirectiveParser(t *testing.T) {
 				require.False(t, rd.Reset)
 				require.False(t, rd.Vars)
 				require.True(t, rd.Help)
+				require.False(t, rd.Rules)
+				require.Nil(t, rd.Exec)
+				require.Nil(t, rd.Load)
+				require.Nil(t, rd.Let)
+			},
+		},
+		{
+			directive: "rules",
+			check: func(t *testing.T, rd *REPLDirective) {
+				t.Helper()
+				require.False(t, rd.Exit)
+				require.False(t, rd.Reset)
+				require.False(t, rd.Vars)
+				require.False(t, rd.Help)
+				require.True(t, rd.Rules)
+				require.Nil(t, rd.Exec)
+				require.Nil(t, rd.Load)
 				require.Nil(t, rd.Let)
 			},
 		},
@@ -147,6 +182,30 @@ func TestDirectiveParser(t *testing.T) {
 				require.Equal(t, `{"k1": [true, false], "k2": {"kk1": true}, "k3": 12}`, strings.TrimSpace(rd.Let.Expr))
 			},
 		},
+		{
+			directive: `load /policies/policy.yaml`,
+			check: func(t *testing.T, rd *REPLDirective) {
+				t.Helper()
+				checkLoadIsDefined(t, rd)
+				require.Equal(t, `/policies/policy.yaml`, strings.TrimSpace(rd.Load.Path))
+			},
+		},
+		{
+			directive: `load ../policies/policy.yaml`,
+			check: func(t *testing.T, rd *REPLDirective) {
+				t.Helper()
+				checkLoadIsDefined(t, rd)
+				require.Equal(t, `../policies/policy.yaml`, strings.TrimSpace(rd.Load.Path))
+			},
+		},
+		{
+			directive: `exec #12`,
+			check: func(t *testing.T, rd *REPLDirective) {
+				t.Helper()
+				checkExecIsDefined(t, rd)
+				require.Equal(t, 12, rd.Exec.RuleID)
+			},
+		},
 	}
 
 	parser, err := NewParser()
@@ -174,5 +233,32 @@ func checkLetIsDefined(t *testing.T, rd *REPLDirective) {
 	require.False(t, rd.Reset)
 	require.False(t, rd.Vars)
 	require.False(t, rd.Help)
+	require.False(t, rd.Rules)
+	require.Nil(t, rd.Exec)
+	require.Nil(t, rd.Load)
 	require.NotNil(t, rd.Let)
 }
+
+func checkLoadIsDefined(t *testing.T, rd *REPLDirective) {
+	t.Helper()
+	require.False(t, rd.Exit)
+	require.False(t, rd.Reset)
+	require.False(t, rd.Vars)
+	require.False(t, rd.Help)
+	require.False(t, rd.Rules)
+	require.Nil(t, rd.Exec)
+	require.Nil(t, rd.Let)
+	require.NotNil(t, rd.Load)
+}
+
+func checkExecIsDefined(t *testing.T, rd *REPLDirective) {
+	t.Helper()
+	require.False(t, rd.Exit)
+	require.False(t, rd.Reset)
+	require.False(t, rd.Vars)
+	require.False(t, rd.Help)
+	require.False(t, rd.Rules)
+	require.Nil(t, rd.Let)
+	require.Nil(t, rd.Load)
+	require.NotNil(t, rd.Exec)
+}
diff --git a/cmd/cerbos/repl/internal/repl_test.go b/cmd/cerbos/repl/internal/repl_test.go
index 74f1bd2b..4e135ab6 100644
--- a/cmd/cerbos/repl/internal/repl_test.go
+++ b/cmd/cerbos/repl/internal/repl_test.go
@@ -307,6 +307,33 @@ func TestREPL(t *testing.T) {
 						}
 					},
 				},
+				{
+					Directive: `V.principal_location`,
+					WantErr:   true,
+				},
+				{
+					Directive: `:let P = {"id":"john","roles":["employee"],"attr":{"ip_address":"10.20.1.2"}}`,
+					Check: func(t *testing.T, m *mockOutput) {
+						t.Helper()
+						require.Equal(t, "P", m.resultName)
+
+						want := &enginev1.Principal{
+							Id:    "john",
+							Roles: []string{"employee"},
+							Attr:  map[string]*structpb.Value{"ip_address": structpb.NewStringValue("10.20.1.2")},
+						}
+
+						require.Empty(t, cmp.Diff(want, m.resultVal.Value(), protocmp.Transform()))
+					},
+				},
+				{
+					Directive: `V.principal_location`,
+					Check: func(t *testing.T, m *mockOutput) {
+						t.Helper()
+						require.Equal(t, lastResultVar, m.resultName)
+						require.Equal(t, "GB", m.resultVal.Value())
+					},
+				},
 			},
 		},
 		{
@@ -361,7 +388,7 @@ type mockOutput struct {
 	yamlObj    any
 	resultName string
 	resultVal  ref.Val
-	rules      []any
+	rules      []proto.Message
 	err        error
 }
 
@@ -379,7 +406,7 @@ func (mo *mockOutput) PrintResult(name string, val ref.Val) {
 	mo.resultVal = val
 }
 
-func (mo *mockOutput) PrintRule(_ int, rule any) error {
+func (mo *mockOutput) PrintRule(_ int, rule proto.Message) error {
 	mo.rules = append(mo.rules, rule)
 	return nil
 }
diff --git a/internal/test/mocks/Index.go b/internal/test/mocks/Index.go
index 80f607ef..2affc27c 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.4. DO NOT EDIT.
+// Code generated by mockery v2.10.6. DO NOT EDIT.
 
 package mocks
 
diff --git a/internal/test/mocks/Store.go b/internal/test/mocks/Store.go
index e58ad1d6..5e874275 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.4. DO NOT EDIT.
+// Code generated by mockery v2.10.6. DO NOT EDIT.
 
 package mocks