policy: update unknown keys normalization

Make the code more unified between validation and
test command. Normalize to key without the input prefix.

Signed-off-by: Tonis Tiigi <tonistiigi@gmail.com>
This commit is contained in:
Tonis Tiigi
2026-02-09 18:27:35 -08:00
parent df542b3f80
commit 4e16277937
4 changed files with 104 additions and 29 deletions
+9 -1
View File
@@ -84,7 +84,7 @@ func runTest(ctx context.Context, out io.Writer, path string, opts policy.TestOp
_, _ = fmt.Fprintln(out, "decision: <nil>") _, _ = fmt.Fprintln(out, "decision: <nil>")
} }
if len(result.MissingInput) > 0 { if len(result.MissingInput) > 0 {
_, _ = fmt.Fprintf(out, "missing_input: %s\n", strings.Join(result.MissingInput, ", ")) _, _ = fmt.Fprintf(out, "missing_input: %s\n", strings.Join(withInputPrefix(result.MissingInput), ", "))
} }
if len(result.MetadataNeeded) > 0 { if len(result.MetadataNeeded) > 0 {
_, _ = fmt.Fprintf(out, "metadata_resolve: %s\n", strings.Join(result.MetadataNeeded, ", ")) _, _ = fmt.Fprintf(out, "metadata_resolve: %s\n", strings.Join(result.MetadataNeeded, ", "))
@@ -106,6 +106,14 @@ func writeJSON(out io.Writer, label string, v any) {
_, _ = fmt.Fprintf(out, "%s:\n%s\n", label, string(dt)) _, _ = fmt.Fprintf(out, "%s:\n%s\n", label, string(dt))
} }
func withInputPrefix(keys []string) []string {
out := make([]string, len(keys))
for i, k := range keys {
out[i] = "input." + k
}
return out
}
type policyTestResolver struct { type policyTestResolver struct {
dockerCli command.Cli dockerCli command.Cli
builderName *string builderName *string
+14 -15
View File
@@ -311,7 +311,7 @@ func runPolicyTest(ctx context.Context, policyModules map[string]*ast.Module, te
result.Allow = allow result.Allow = allow
result.DenyMessages = deny result.DenyMessages = deny
missing := missingInputRefs(policyPackageModules, effectiveInput) missing := missingInputRefs(policyPackageModules, effectiveInput, runtimeUnknownInputRefs(testState), runtimeUnknownInputRefs(decisionState))
result.MissingInput = uniqueSortedStrings(missing) result.MissingInput = uniqueSortedStrings(missing)
result.MetadataNeeded = summarizeMetadataRequests(result.MissingInput) result.MetadataNeeded = summarizeMetadataRequests(result.MissingInput)
@@ -534,7 +534,7 @@ func hasEnv(env Env) bool {
func filterResolvableMissing(missing []string) []string { func filterResolvableMissing(missing []string) []string {
out := make([]string, 0, len(missing)) out := make([]string, 0, len(missing))
for _, m := range missing { for _, m := range missing {
if strings.HasPrefix(m, "input.image.") || strings.HasPrefix(m, "input.git.") { if strings.HasPrefix(m, "image.") || strings.HasPrefix(m, "git.") {
out = append(out, m) out = append(out, m)
} }
} }
@@ -595,24 +595,27 @@ func modulesForPackage(modules map[string]*ast.Module, pkgPath string) []*ast.Mo
return out return out
} }
func missingInputRefs(mods []*ast.Module, input *Input) []string { func missingInputRefs(mods []*ast.Module, input *Input, extraRefs ...[]string) []string {
if len(mods) == 0 { if len(mods) == 0 {
return nil return nil
} }
inputMap := normalizeInput(input) inputMap := normalizeInput(input)
refs := collectUnknowns(mods, nil) refs := collectUnknowns(mods, nil)
for _, er := range extraRefs {
refs = append(refs, er...)
}
seen := map[string]struct{}{}
missing := make([]string, 0, len(refs)) missing := make([]string, 0, len(refs))
for _, ref := range refs { for _, key := range refs {
key := strings.TrimPrefix(ref, "input.")
if key == ref {
continue
}
key = trimKey(key)
if key == "" { if key == "" {
continue continue
} }
if _, ok := seen[key]; ok {
continue
}
seen[key] = struct{}{}
if !inputHasPath(inputMap, strings.Split(key, ".")) { if !inputHasPath(inputMap, strings.Split(key, ".")) {
missing = append(missing, "input."+key) missing = append(missing, key)
} }
} }
return missing return missing
@@ -703,11 +706,7 @@ func summarizeMetadataRequests(missing []string) []string {
return nil return nil
} }
req := &gwpb.ResolveSourceMetaRequest{} req := &gwpb.ResolveSourceMetaRequest{}
trimmed := make([]string, 0, len(missing)) if err := AddUnknowns(req, missing); err != nil {
for _, m := range missing {
trimmed = append(trimmed, strings.TrimPrefix(m, "input."))
}
if err := AddUnknowns(req, trimmed); err != nil {
return nil return nil
} }
var out []string var out []string
+53
View File
@@ -3,6 +3,7 @@ package policy
import ( import (
"testing" "testing"
"github.com/open-policy-agent/opa/v1/ast"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -18,12 +19,16 @@ func TestTrimKey(t *testing.T) {
// one separator → stays as-is // one separator → stays as-is
{"git.tag", "git.tag"}, {"git.tag", "git.tag"},
{"git[tag", "git[tag"}, {"git[tag", "git[tag"},
{"input.git.tag", "git.tag"},
{"input.git[tag", "git[tag"},
// multiple separators → cut before second one // multiple separators → cut before second one
{"git.tag.author", "git.tag"}, {"git.tag.author", "git.tag"},
{"git.tag.author.email", "git.tag"}, {"git.tag.author.email", "git.tag"},
{"git.tag[0][1]", "git.tag"}, {"git.tag[0][1]", "git.tag"},
{"git.tag[0]", "git.tag"}, {"git.tag[0]", "git.tag"},
{"input.git.tag.author", "git.tag"},
{"input.git.tag[0]", "git.tag"},
{"a.b.c", "a.b"}, {"a.b.c", "a.b"},
} }
@@ -34,3 +39,51 @@ func TestTrimKey(t *testing.T) {
}) })
} }
} }
func TestCollectUnknowns(t *testing.T) {
mod, err := ast.ParseModule("x.rego", `
package x
p if {
input.git.tag[0].author == "a"
input.image.signatures[_].signer.certificateIssuer != ""
data.foo.bar == 1
}
`)
require.NoError(t, err)
all := collectUnknowns([]*ast.Module{mod}, nil)
require.ElementsMatch(t, []string{"git.tag", "image.signatures"}, all)
filtered := collectUnknowns([]*ast.Module{mod}, []string{"input.image.signatures"})
require.Equal(t, []string{"image.signatures"}, filtered)
}
func TestRuntimeUnknownInputRefs(t *testing.T) {
require.Nil(t, runtimeUnknownInputRefs(nil))
require.Nil(t, runtimeUnknownInputRefs(&state{}))
st := &state{
Unknowns: map[string]struct{}{
funcVerifyGitSignature: {},
},
}
require.Equal(t, []string{"git.commit"}, runtimeUnknownInputRefs(st))
}
func TestMissingInputRefsWithRuntimeUnknowns(t *testing.T) {
mod, err := ast.ParseModule("x.rego", `
package x
p if {
input.git.ref != ""
}
`)
require.NoError(t, err)
in := &Input{
Git: &Git{
Ref: "refs/heads/main",
},
}
missing := missingInputRefs([]*ast.Module{mod}, in, []string{"git.commit"})
require.Equal(t, []string{"git.commit"}, missing)
}
+28 -13
View File
@@ -224,9 +224,7 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy
return nil, nil, err return nil, nil, err
} }
unk := collectUnknowns(pq.Support, unknowns) unk := collectUnknowns(pq.Support, unknowns)
if _, ok := st.Unknowns[funcVerifyGitSignature]; ok { unk = append(unk, runtimeUnknownInputRefs(st)...)
unk = append(unk, "input.git.commit")
}
if len(unk) > 0 { if len(unk) > 0 {
next := &gwpb.ResolveSourceMetaRequest{ next := &gwpb.ResolveSourceMetaRequest{
@@ -621,14 +619,12 @@ func AddUnknowns(req *gwpb.ResolveSourceMetaRequest, unk []string) error {
func AddUnknownsWithLogger(logf func(logrus.Level, string), req *gwpb.ResolveSourceMetaRequest, unk []string) error { func AddUnknownsWithLogger(logf func(logrus.Level, string), req *gwpb.ResolveSourceMetaRequest, unk []string) error {
unk2 := make([]string, 0, len(unk)) unk2 := make([]string, 0, len(unk))
for _, u := range unk { for _, u := range unk {
k := strings.TrimPrefix(u, "input.") switch u {
k = trimKey(k)
switch k {
case "image", "git", "http", "local": case "image", "git", "http", "local":
// parents are returned as unknowns for some reason, ignore // parents are returned as unknowns for some reason, ignore
continue continue
default: default:
unk2 = append(unk2, k) unk2 = append(unk2, u)
} }
} }
if len(unk2) == 0 { if len(unk2) == 0 {
@@ -680,8 +676,10 @@ func collectUnknowns(mods []*ast.Module, allowed []string) []string {
for _, mod := range mods { for _, mod := range mods {
ast.WalkRefs(mod, func(ref ast.Ref) bool { ast.WalkRefs(mod, func(ref ast.Ref) bool {
if ref.HasPrefix(ast.InputRootRef) { if ref.HasPrefix(ast.InputRootRef) {
s := ref.String() // e.g. "input.request.path" s := trimKey(ref.String())
s = "input." + trimKey(strings.TrimPrefix(s, "input.")) if s == "" {
return true
}
if _, ok := seen[s]; !ok { if _, ok := seen[s]; !ok {
seen[s] = struct{}{} seen[s] = struct{}{}
out = append(out, s) out = append(out, s)
@@ -696,6 +694,10 @@ func collectUnknowns(mods []*ast.Module, allowed []string) []string {
valid := map[string]struct{}{} valid := map[string]struct{}{}
for _, k := range allowed { for _, k := range allowed {
k = trimKey(k)
if k == "" {
continue
}
valid[k] = struct{}{} valid[k] = struct{}{}
} }
@@ -709,14 +711,25 @@ func collectUnknowns(mods []*ast.Module, allowed []string) []string {
return filtered return filtered
} }
func runtimeUnknownInputRefs(st *state) []string {
if st == nil || len(st.Unknowns) == 0 {
return nil
}
var out []string
if _, ok := st.Unknowns[funcVerifyGitSignature]; ok {
out = append(out, "git.commit")
}
return out
}
func summarizeUnknownsForLog(unk []string) []string { func summarizeUnknownsForLog(unk []string) []string {
out := make([]string, 0, len(unk)) out := make([]string, 0, len(unk))
seen := map[string]struct{}{} seen := map[string]struct{}{}
for _, u := range unk { for _, u := range unk {
if strings.HasPrefix(u, "input.image.signatures") { if strings.HasPrefix(u, "image.signatures") {
u = "input.image.signatures" u = "image.signatures"
} }
if u == "input.image" { if u == "image" {
continue continue
} }
if _, ok := seen[u]; ok { if _, ok := seen[u]; ok {
@@ -730,7 +743,7 @@ func summarizeUnknownsForLog(unk []string) []string {
func hasHTTPUnknowns(unk []string) bool { func hasHTTPUnknowns(unk []string) bool {
for _, u := range unk { for _, u := range unk {
if strings.HasPrefix(u, "input.http.") { if strings.HasPrefix(u, "http.") {
return true return true
} }
} }
@@ -738,6 +751,8 @@ func hasHTTPUnknowns(unk []string) bool {
} }
func trimKey(s string) string { func trimKey(s string) string {
s = strings.TrimPrefix(s, "input.")
const ( const (
dot = '.' dot = '.'
sb = '[' sb = '['