1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-04-08 10:04:25 +00:00

redo variable validation ()

* redo variable validation

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* handle quotes for JMESPath - escaping

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix tests and linter issues

Signed-off-by: Jim Bugwadia <jim@nirmata.com>

* fix fmt

Signed-off-by: Jim Bugwadia <jim@nirmata.com>
This commit is contained in:
Jim Bugwadia 2021-11-03 11:16:55 -07:00 committed by GitHub
parent 40d30df726
commit 5c16ee738a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 416 additions and 401 deletions

View file

@ -2,7 +2,6 @@ package context
import (
"encoding/json"
"fmt"
"strings"
"sync"
@ -56,17 +55,14 @@ type Context struct {
mutex sync.RWMutex
jsonRaw []byte
jsonRawCheckpoints [][]byte
builtInVars []string
images *Images
log logr.Logger
}
//NewContext returns a new context
// builtInVars is the list of known variables (e.g. serviceAccountName)
func NewContext(builtInVars ...string) *Context {
func NewContext() *Context {
ctx := Context{
jsonRaw: []byte(`{}`), // empty json struct
builtInVars: builtInVars,
log: log.Log.WithName("context"),
jsonRawCheckpoints: make([][]byte, 0),
}
@ -74,16 +70,6 @@ func NewContext(builtInVars ...string) *Context {
return &ctx
}
// InvalidVariableErr represents error for non-white-listed variables
type InvalidVariableErr struct {
variable string
whiteList []string
}
func (i InvalidVariableErr) Error() string {
return fmt.Sprintf("variable %s cannot be used, allowed variables: %v", i.variable, i.whiteList)
}
// AddJSON merges json data
func (ctx *Context) AddJSON(dataRaw []byte) error {
var err error
@ -359,20 +345,3 @@ func (ctx *Context) reset(remove bool) {
ctx.jsonRawCheckpoints = ctx.jsonRawCheckpoints[:n]
}
}
// AddBuiltInVars adds given pattern to the builtInVars
func (ctx *Context) AddBuiltInVars(pattern string) {
ctx.mutex.Lock()
defer ctx.mutex.Unlock()
builtInVarsCopy := ctx.builtInVars
ctx.builtInVars = append(builtInVarsCopy, pattern)
}
func (ctx *Context) getBuiltInVars() []string {
ctx.mutex.RLock()
defer ctx.mutex.RUnlock()
vars := ctx.builtInVars
return vars
}

View file

@ -19,13 +19,6 @@ func (ctx *Context) Query(query string) (interface{}, error) {
}
var emptyResult interface{}
// check for white-listed variables
if !ctx.isBuiltInVariable(query) {
return emptyResult, InvalidVariableErr{
variable: query,
whiteList: ctx.getBuiltInVars(),
}
}
// compile the query
queryPath, err := jmespath.New(query)
@ -33,6 +26,7 @@ func (ctx *Context) Query(query string) (interface{}, error) {
ctx.log.Error(err, "incorrect query", "query", query)
return emptyResult, fmt.Errorf("incorrect query %s: %v", query, err)
}
// search
ctx.mutex.RLock()
defer ctx.mutex.RUnlock()
@ -55,18 +49,6 @@ func (ctx *Context) Query(query string) (interface{}, error) {
return result, nil
}
func (ctx *Context) isBuiltInVariable(variable string) bool {
if len(ctx.getBuiltInVars()) == 0 {
return true
}
for _, wVar := range ctx.getBuiltInVars() {
if strings.HasPrefix(variable, wVar) {
return true
}
}
return false
}
func (ctx *Context) HasChanged(jmespath string) (bool, error) {
objData, err := ctx.Query("request.object." + jmespath)
if err != nil {

View file

@ -0,0 +1,101 @@
package context
import (
"fmt"
"regexp"
"strings"
"sync"
"github.com/kyverno/kyverno/pkg/engine/jmespath"
"github.com/minio/pkg/wildcard"
)
//MockContext is used for testing and validation of variables
type MockContext struct {
mutex sync.RWMutex
re *regexp.Regexp
allowedPatterns []string
}
//NewMockContext creates a new MockContext that allows variables matching the supplied list of wildcard patterns
func NewMockContext(re *regexp.Regexp, vars ...string) *MockContext {
return &MockContext{re: re, allowedPatterns: vars}
}
// AddVariable adds given wildcardPattern to the allowed variable patterns
func (ctx *MockContext) AddVariable(wildcardPattern string) {
ctx.mutex.Lock()
defer ctx.mutex.Unlock()
builtInVarsCopy := ctx.allowedPatterns
ctx.allowedPatterns = append(builtInVarsCopy, wildcardPattern)
}
//Query the JSON context with JMESPATH search path
func (ctx *MockContext) Query(query string) (interface{}, error) {
query = strings.TrimSpace(query)
if query == "" {
return nil, fmt.Errorf("invalid query (nil)")
}
var emptyResult interface{}
// compile the query
_, err := jmespath.New(query)
if err != nil {
return emptyResult, fmt.Errorf("invalid JMESPath query %s: %v", query, err)
}
// strip escaped quotes from JMESPath variables with dashes e.g. {{ \"my-map.data\".key }}
query = strings.Replace(query, "\"", "", -1)
if ctx.re != nil && ctx.re.MatchString(query) {
return emptyResult, nil
}
if ctx.isVariableDefined(query) {
return emptyResult, nil
}
return emptyResult, InvalidVariableErr{
variable: query,
re: ctx.re,
allowedPatterns: ctx.allowedPatterns,
}
}
func (ctx *MockContext) isVariableDefined(variable string) bool {
for _, pattern := range ctx.getVariables() {
if wildcard.Match(pattern, variable) {
return true
}
}
return false
}
func (ctx *MockContext) getVariables() []string {
ctx.mutex.RLock()
defer ctx.mutex.RUnlock()
vars := ctx.allowedPatterns
return vars
}
// InvalidVariableErr represents error for non-white-listed variables
type InvalidVariableErr struct {
variable string
re *regexp.Regexp
allowedPatterns []string
}
func (i InvalidVariableErr) Error() string {
if i.re == nil {
return fmt.Sprintf("variable %s must match patterns %v", i.variable, i.allowedPatterns)
}
return fmt.Sprintf("variable %s must match regex \"%s\" or patterns %v", i.variable, i.re.String(), i.allowedPatterns)
}
func (ctx *MockContext) HasChanged(_ string) (bool, error) {
return false, nil
}

View file

@ -212,49 +212,10 @@ func substituteReferences(log logr.Logger, rule interface{}) (interface{}, error
return jsonUtils.NewTraversal(rule, substituteReferencesIfAny(log)).TraverseJSON()
}
// ValidateBackgroundModeVars validates variables against the specified context,
// which contains a list of allowed JMESPath queries in background processing,
// and throws an error if the variable is not allowed.
func ValidateBackgroundModeVars(log logr.Logger, ctx context.EvalInterface, rule interface{}) (interface{}, error) {
return jsonUtils.NewTraversal(rule, validateBackgroundModeVars(log, ctx)).TraverseJSON()
}
func ValidateElementInForEach(log logr.Logger, rule interface{}) (interface{}, error) {
return jsonUtils.NewTraversal(rule, validateElementInForEach(log)).TraverseJSON()
}
func validateBackgroundModeVars(log logr.Logger, ctx context.EvalInterface) jsonUtils.Action {
return jsonUtils.OnlyForLeafsAndKeys(func(data *jsonUtils.ActionData) (interface{}, error) {
value, ok := data.Element.(string)
if !ok {
return data.Element, nil
}
vars := RegexVariables.FindAllString(value, -1)
for _, v := range vars {
initial := len(regexVariableInit.FindAllString(v, -1)) > 0
if !initial {
v = v[1:]
}
variable := replaceBracesAndTrimSpaces(v)
_, err := ctx.Query(variable)
if err != nil {
switch err.(type) {
case gojmespath.NotFoundError:
return nil, nil
case context.InvalidVariableErr:
return nil, err
default:
return nil, fmt.Errorf("failed to resolve %v at path %s: %v", variable, data.Path, err)
}
}
}
return nil, nil
})
}
func validateElementInForEach(log logr.Logger) jsonUtils.Action {
return jsonUtils.OnlyForLeafsAndKeys(func(data *jsonUtils.ActionData) (interface{}, error) {
value, ok := data.Element.(string)
@ -379,7 +340,15 @@ func substituteVariablesIfAny(log logr.Logger, ctx context.EvalInterface, vr Var
variable := replaceBracesAndTrimSpaces(v)
if variable == "@" {
variable = strings.Replace(variable, "@", fmt.Sprintf("request.object.%s", getJMESPath(data.Path)), -1)
path := getJMESPath(data.Path)
var val string
if strings.HasPrefix(path, "[") {
val = fmt.Sprintf("request.object%s", path)
} else {
val = fmt.Sprintf("request.object.%s", path)
}
variable = strings.Replace(variable, "@", val, -1)
}
if isDeleteRequest {
@ -441,12 +410,15 @@ func isDeleteRequest(ctx context.EvalInterface) bool {
return false
}
// getJMESPath converts path to JMES format
var regexPathDigit = regexp.MustCompile(`\.?([\d])\.?`)
// getJMESPath converts path to JMESPath format
func getJMESPath(rawPath string) string {
tokens := strings.Split(rawPath, "/")[3:] // skip empty element and two non-resource (like mutate.overlay)
tokens := strings.Split(rawPath, "/")[3:] // skip "/" + 2 elements (e.g. mutate.overlay | validate.pattern)
path := strings.Join(tokens, ".")
regex := regexp.MustCompile(`\.([\d])\.`)
return string(regex.ReplaceAll([]byte(path), []byte("[$1].")))
b := regexPathDigit.ReplaceAll([]byte(path), []byte("[$1]."))
result := strings.Trim(string(b), ".")
return result
}
func substituteVarInPattern(prefix, pattern, variable string, value interface{}) (string, error) {

View file

@ -526,7 +526,7 @@ func Test_policyContextValidation(t *testing.T) {
err := json.Unmarshal(policyContext, &contextMap)
assert.NilError(t, err)
ctx := context.NewContext("request.object")
ctx := context.NewMockContext(nil, "request.object")
_, err = SubstituteAll(log.Log, ctx, contextMap)
assert.Assert(t, err != nil, err)
@ -603,7 +603,7 @@ func Test_variableSubstitution_array(t *testing.T) {
err := json.Unmarshal(ruleRaw, &rule)
assert.NilError(t, err)
ctx := context.NewContext("request.object", "animals")
ctx := context.NewContext()
ctx.AddJSON(configmapRaw)
ctx.AddResource(resourceRaw)
@ -982,7 +982,7 @@ func TestFormAbsolutePath_RelativePathExists(t *testing.T) {
assert.Assert(t, result == expectedString)
}
func TestFormAbsolutePath_RelativePathWithBackToTopInTheBegining(t *testing.T) {
func TestFormAbsolutePath_RelativePathWithBackToTopInTheBeginning(t *testing.T) {
absolutePath := "/spec/containers/0/resources/requests/memory"
referencePath := "../../limits/memory"
expectedString := "/spec/containers/0/resources/limits/memory"
@ -1151,3 +1151,9 @@ func Test_ReplacingEscpNestedVariableWhenDeleting(t *testing.T) {
assert.Equal(t, fmt.Sprintf("%v", pattern), "{{request.object.metadata.annotations.target}}")
}
func Test_getJMESPath(t *testing.T) {
assert.Equal(t, "spec.containers[0]", getJMESPath("/validate/pattern/spec/containers/0"))
assert.Equal(t, "spec.containers[0].volumes[1]", getJMESPath("/validate/pattern/spec/containers/0/volumes/1"))
assert.Equal(t, "[0]", getJMESPath("/mutate/overlay/0"))
}

View file

@ -280,7 +280,7 @@ func applyCommandHelper(resourcePaths []string, cluster bool, policyReport bool,
continue
}
matches := common.PolicyHasVariables(*policy)
matches := common.HasVariables(policy)
variable := common.RemoveDuplicateAndObjectVariables(matches)
if len(variable) > 0 {
if len(variables) == 0 {

View file

@ -13,6 +13,8 @@ import (
"reflect"
"strings"
"github.com/kyverno/kyverno/pkg/engine/variables"
jsonpatch "github.com/evanphx/json-patch/v5"
"github.com/go-git/go-billy/v5"
"github.com/go-logr/logr"
@ -23,7 +25,6 @@ import (
"github.com/kyverno/kyverno/pkg/engine"
"github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/response"
"github.com/kyverno/kyverno/pkg/engine/variables"
sanitizederror "github.com/kyverno/kyverno/pkg/kyverno/sanitizedError"
"github.com/kyverno/kyverno/pkg/kyverno/store"
"github.com/kyverno/kyverno/pkg/policymutation"
@ -71,6 +72,13 @@ type NamespaceSelector struct {
Labels map[string]string `json:"labels"`
}
// HasVariables - check for variables in the policy
func HasVariables(policy *v1.ClusterPolicy) [][]string {
policyRaw, _ := json.Marshal(policy)
matches := variables.RegexVariables.FindAllStringSubmatch(string(policyRaw), -1)
return matches
}
// GetPolicies - Extracting the policies from multiple YAML
func GetPolicies(paths []string) (policies []*v1.ClusterPolicy, errors []error) {
for _, path := range paths {
@ -165,107 +173,6 @@ func GetPolicies(paths []string) (policies []*v1.ClusterPolicy, errors []error)
return policies, errors
}
// PolicyHasVariables - check for variables in the policy
func PolicyHasVariables(policy v1.ClusterPolicy) [][]string {
policyRaw, _ := json.Marshal(policy)
matches := RegexVariables.FindAllStringSubmatch(string(policyRaw), -1)
return matches
}
// for now forbidden sections are match, exclude and
func ruleForbiddenSectionsHaveVariables(rule *v1.Rule) error {
var err error
err = JSONPatchPathHasVariables(rule.Mutation.PatchesJSON6902)
if err != nil {
return fmt.Errorf("Rule \"%s\" should not have variables in patchesJSON6902 path section", rule.Name)
}
err = objectHasVariables(rule.ExcludeResources)
if err != nil {
return fmt.Errorf("Rule \"%s\" should not have variables in exclude section", rule.Name)
}
err = objectHasVariables(rule.MatchResources)
if err != nil {
return fmt.Errorf("Rule \"%s\" should not have variables in match section", rule.Name)
}
return nil
}
func JSONPatchPathHasVariables(patch string) error {
jsonPatch, err := yaml.ToJSON([]byte(patch))
if err != nil {
return err
}
decodedPatch, err := jsonpatch.DecodePatch(jsonPatch)
if err != nil {
return err
}
for _, operation := range decodedPatch {
path, err := operation.Path()
if err != nil {
return err
}
vars := variables.RegexVariables.FindAllString(path, -1)
if len(vars) > 0 {
return fmt.Errorf("Operation \"%s\" has forbidden variables", operation.Kind())
}
}
return nil
}
func objectHasVariables(object interface{}) error {
var err error
objectJSON, err := json.Marshal(object)
if err != nil {
return err
}
if len(RegexVariables.FindAllStringSubmatch(string(objectJSON), -1)) > 0 {
return fmt.Errorf("Object has forbidden variables")
}
return nil
}
// PolicyHasNonAllowedVariables - checks for unexpected variables in the policy
func PolicyHasNonAllowedVariables(policy v1.ClusterPolicy) error {
for _, r := range policy.Spec.Rules {
rule := r.DeepCopy()
// do not validate attestation variables as they are based on external data
for _, vi := range rule.VerifyImages {
vi.Attestations = nil
}
var err error
ruleJSON, err := json.Marshal(rule)
if err != nil {
return err
}
err = ruleForbiddenSectionsHaveVariables(rule)
if err != nil {
return err
}
matchesAll := RegexVariables.FindAllStringSubmatch(string(ruleJSON), -1)
matchesAllowed := AllowedVariables.FindAllStringSubmatch(string(ruleJSON), -1)
if (len(matchesAll) > len(matchesAllowed)) && len(rule.Context) == 0 {
allowed := "{{request.*}}, {{element.*}}, {{serviceAccountName}}, {{serviceAccountNamespace}}, {{@}}, {{images.*}} and context variables"
return fmt.Errorf("rule \"%s\" has forbidden variables. Allowed variables are: %s", rule.Name, allowed)
}
}
return nil
}
// MutatePolicy - applies mutation to a policy
func MutatePolicy(policy *v1.ClusterPolicy, logger logr.Logger) (*v1.ClusterPolicy, error) {
patches, _ := policymutation.GenerateJSONPatchesForDefaults(policy, logger)

View file

@ -7,11 +7,5 @@ import (
// RegexVariables represents regex for '{{}}'
var RegexVariables = regexp.MustCompile(`\{\{[^{}]*\}\}`)
// AllowedVariables represents regex for {{request.}}, {{serviceAccountName}}, {{serviceAccountNamespace}}, {{@}}, {{element.}}, {{images.}}
var AllowedVariables = regexp.MustCompile(`\{\{\s*(request\.|serviceAccountName|serviceAccountNamespace|element\.|@|images\.|([a-z_0-9]+\())[^{}]*\}\}`)
// WildCardAllowedVariables represents regex for the allowed fields in wildcards
var WildCardAllowedVariables = regexp.MustCompile(`\{\{\s*(request\.|serviceAccountName|serviceAccountNamespace)[^{}]*\}\}`)
// IsHTTPRegex represents regex for starts with http:// or https://
var IsHTTPRegex = regexp.MustCompile("^(http|https)://")

View file

@ -650,7 +650,7 @@ func applyPoliciesFromPath(fs billy.Filesystem, policyBytes []byte, valuesFile s
continue
}
matches := common.PolicyHasVariables(*policy)
matches := common.HasVariables(policy)
variable := common.RemoveDuplicateAndObjectVariables(matches)
if len(variable) > 0 {

View file

@ -1,4 +1,4 @@
package common
package policy
import (
"fmt"
@ -57,8 +57,8 @@ func TestNotAllowedVars_MatchSection(t *testing.T) {
policy, err := ut.GetPolicy(policyWithVarInMatch)
assert.NilError(t, err)
err = PolicyHasNonAllowedVariables(*policy[0])
assert.Error(t, err, "Rule \"validate-name\" should not have variables in match section")
err = hasInvalidVariables(policy[0], false)
assert.Error(t, err, "rule \"validate-name\" should not have variables in match section")
}
func TestNotAllowedVars_ExcludeSection(t *testing.T) {
@ -109,8 +109,8 @@ func TestNotAllowedVars_ExcludeSection(t *testing.T) {
policy, err := ut.GetPolicy(policyWithVarInExclude)
assert.NilError(t, err)
err = PolicyHasNonAllowedVariables(*policy[0])
assert.Error(t, err, "Rule \"validate-name\" should not have variables in exclude section")
err = hasInvalidVariables(policy[0], false)
assert.Error(t, err, "rule \"validate-name\" should not have variables in exclude section")
}
func TestNotAllowedVars_ExcludeSection_PositiveCase(t *testing.T) {
@ -162,7 +162,7 @@ func TestNotAllowedVars_ExcludeSection_PositiveCase(t *testing.T) {
policy, err := ut.GetPolicy(policyWithVarInExclude)
assert.NilError(t, err)
err = PolicyHasNonAllowedVariables(*policy[0])
err = hasInvalidVariables(policy[0], false)
assert.NilError(t, err)
}
@ -196,8 +196,8 @@ func TestNotAllowedVars_JSONPatchPath(t *testing.T) {
policy, err := ut.GetPolicy(policyWithVarInExclude)
assert.NilError(t, err)
err = PolicyHasNonAllowedVariables(*policy[0])
assert.Error(t, err, "Rule \"pCM1\" should not have variables in patchesJSON6902 path section")
err = hasInvalidVariables(policy[0], false)
assert.Error(t, err, "rule \"pCM1\" should not have variables in patchesJSON6902 path section")
}
func TestNotAllowedVars_JSONPatchPath_PositiveCase(t *testing.T) {
@ -230,7 +230,7 @@ func TestNotAllowedVars_JSONPatchPath_PositiveCase(t *testing.T) {
policy, err := ut.GetPolicy(policyWithVarInExclude)
assert.NilError(t, err)
err = PolicyHasNonAllowedVariables(*policy[0])
err = hasInvalidVariables(policy[0], false)
assert.NilError(t, err)
}
@ -262,7 +262,7 @@ spec:
policy, err := ut.GetPolicy(policyJSON)
assert.NilError(t, err)
err = PolicyHasNonAllowedVariables(*policy[0])
err = hasInvalidVariables(policy[0], false)
assert.NilError(t, err)
}
@ -276,7 +276,7 @@ func TestNotAllowedVars_VariableFormats(t *testing.T) {
{"request_object", "request.object.meta", true},
{"service_account_name", "serviceAccountName", true},
{"service_account_namespace", "serviceAccountNamespace", true},
{"@", "@", true},
{"self", "@", true},
{"custom_func_compare", "compare(string, string)", true},
{"custom_func_contains", "contains(string, string)", true},
{"custom_func_equal_fold", "equal_fold(string, string)", true},
@ -316,6 +316,7 @@ func TestNotAllowedVars_VariableFormats(t *testing.T) {
{"to_number", "to_number(foo, bar)", true},
{"type", "type(foo, bar)", true},
{"values", "values(foo, bar)", true},
{"self_path_test", "@", true},
}
for _, tc := range tcs {
@ -348,7 +349,7 @@ func TestNotAllowedVars_VariableFormats(t *testing.T) {
policy, err := ut.GetPolicy(policyYAML)
assert.NilError(t, err)
err = PolicyHasNonAllowedVariables(*policy[0])
err = hasInvalidVariables(policy[0], false)
if tc.pass {
assert.NilError(t, err, "%s: not expecting an error", tc.name)
} else {

View file

@ -3,89 +3,69 @@ package policy
import (
"encoding/json"
"fmt"
"strings"
gojmespath "github.com/jmespath/go-jmespath"
kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
"github.com/kyverno/kyverno/pkg/engine/context"
"github.com/kyverno/kyverno/pkg/engine/variables"
"github.com/kyverno/kyverno/pkg/utils"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"sigs.k8s.io/controller-runtime/pkg/log"
)
//ContainsVariablesOtherThanObject returns error if variable that does not start from request.object
func ContainsVariablesOtherThanObject(policy kyverno.ClusterPolicy) error {
var err error
for idx, rule := range policy.Spec.Rules {
if path := userInfoDefined(rule.MatchResources.UserInfo); path != "" {
return fmt.Errorf("invalid variable used at path: spec/rules[%d]/match/%s", idx, path)
//ContainsUserVariables returns error if variable that does not start from request.object
func containsUserVariables(policy *kyverno.ClusterPolicy, vars [][]string) error {
for _, s := range vars {
if strings.Contains(s[0], "userInfo") {
return fmt.Errorf("variable %s is not allowed", s[0])
}
}
if path := userInfoDefined(rule.ExcludeResources.UserInfo); path != "" {
return fmt.Errorf("invalid variable used at path: spec/rules[%d]/exclude/%s", idx, path)
}
if len(rule.MatchResources.Any) > 0 {
for i, value := range rule.MatchResources.Any {
if path := userInfoDefined(value.UserInfo); path != "" {
return fmt.Errorf("invalid variable used at path: spec/rules[%d]/match/any[%d]/%s", idx, i, path)
}
}
}
if len(rule.MatchResources.All) > 0 {
for i, value := range rule.MatchResources.All {
if path := userInfoDefined(value.UserInfo); path != "" {
return fmt.Errorf("invalid variable used at path: spec/rules[%d]/match/all[%d]/%s", idx, i, path)
}
}
}
if len(rule.ExcludeResources.All) > 0 {
for i, value := range rule.ExcludeResources.All {
if path := userInfoDefined(value.UserInfo); path != "" {
return fmt.Errorf("invalid variable used at path: spec/rules[%d]/exclude/any[%d]/%s", idx, i, path)
}
}
}
if len(rule.ExcludeResources.Any) > 0 {
for i, value := range rule.ExcludeResources.Any {
if path := userInfoDefined(value.UserInfo); path != "" {
return fmt.Errorf("invalid variable used at path: spec/rules[%d]/exclude/all[%d]/%s", idx, i, path)
}
}
}
filterVars := []string{"request.object", "request.namespace", "images", "element"}
ctx := context.NewContext(filterVars...)
for _, contextEntry := range rule.Context {
if contextEntry.APICall != nil {
ctx.AddBuiltInVars(contextEntry.Name)
}
if contextEntry.ConfigMap != nil {
ctx.AddBuiltInVars(contextEntry.Name)
}
}
err = validateBackgroundModeVars(ctx, rule)
if err != nil {
for idx := range policy.Spec.Rules {
if err := hasUserMatchExclude(idx, &policy.Spec.Rules[idx]); err != nil {
return err
}
if rule, err = variables.SubstituteAllInRule(log.Log, ctx, rule); !checkNotFoundErr(err) {
return fmt.Errorf("variable substitution failed for rule %s: %s", rule.Name, err.Error())
}
}
if rule.AnyAllConditions != nil {
if err = validatePreConditions(idx, ctx, rule.AnyAllConditions); !checkNotFoundErr(err) {
return err
return nil
}
func hasUserMatchExclude(idx int, rule *kyverno.Rule) error {
if path := userInfoDefined(rule.MatchResources.UserInfo); path != "" {
return fmt.Errorf("invalid variable used at path: spec/rules[%d]/match/%s", idx, path)
}
if path := userInfoDefined(rule.ExcludeResources.UserInfo); path != "" {
return fmt.Errorf("invalid variable used at path: spec/rules[%d]/exclude/%s", idx, path)
}
if len(rule.MatchResources.Any) > 0 {
for i, value := range rule.MatchResources.Any {
if path := userInfoDefined(value.UserInfo); path != "" {
return fmt.Errorf("invalid variable used at path: spec/rules[%d]/match/any[%d]/%s", idx, i, path)
}
}
}
if rule.Validation.Deny != nil {
if err = validateDenyConditions(idx, ctx, rule.Validation.Deny.AnyAllConditions); !checkNotFoundErr(err) {
return err
if len(rule.MatchResources.All) > 0 {
for i, value := range rule.MatchResources.All {
if path := userInfoDefined(value.UserInfo); path != "" {
return fmt.Errorf("invalid variable used at path: spec/rules[%d]/match/all[%d]/%s", idx, i, path)
}
}
}
if len(rule.ExcludeResources.All) > 0 {
for i, value := range rule.ExcludeResources.All {
if path := userInfoDefined(value.UserInfo); path != "" {
return fmt.Errorf("invalid variable used at path: spec/rules[%d]/exclude/any[%d]/%s", idx, i, path)
}
}
}
if len(rule.ExcludeResources.Any) > 0 {
for i, value := range rule.ExcludeResources.Any {
if path := userInfoDefined(value.UserInfo); path != "" {
return fmt.Errorf("invalid variable used at path: spec/rules[%d]/exclude/all[%d]/%s", idx, i, path)
}
}
}
@ -93,54 +73,6 @@ func ContainsVariablesOtherThanObject(policy kyverno.ClusterPolicy) error {
return nil
}
func validatePreConditions(idx int, ctx context.EvalInterface, anyAllConditions apiextensions.JSON) error {
var err error
anyAllConditions, err = substituteVarsInJSON(ctx, anyAllConditions)
if err != nil {
return err
}
_, err = utils.ApiextensionsJsonToKyvernoConditions(anyAllConditions)
if err != nil {
return err
}
return nil
}
func validateDenyConditions(idx int, ctx context.EvalInterface, denyConditions apiextensions.JSON) error {
var err error
denyConditions, err = substituteVarsInJSON(ctx, denyConditions)
if err != nil {
return err
}
_, err = utils.ApiextensionsJsonToKyvernoConditions(denyConditions)
if err != nil {
return err
}
return nil
}
func checkNotFoundErr(err error) bool {
if err != nil {
switch err.(type) {
case gojmespath.NotFoundError:
return true
case context.InvalidVariableErr:
// non-white-listed variable is found
return false
default:
return false
}
}
return true
}
func userInfoDefined(ui kyverno.UserInfo) string {
if len(ui.Roles) > 0 {
return "roles"
@ -154,21 +86,6 @@ func userInfoDefined(ui kyverno.UserInfo) string {
return ""
}
func validateBackgroundModeVars(ctx context.EvalInterface, document apiextensions.JSON) error {
jsonByte, err := json.Marshal(document)
if err != nil {
return err
}
var jsonInterface interface{}
err = json.Unmarshal(jsonByte, &jsonInterface)
if err != nil {
return err
}
_, err = variables.ValidateBackgroundModeVars(log.Log, ctx, jsonInterface)
return err
}
func substituteVarsInJSON(ctx context.EvalInterface, document apiextensions.JSON) (apiextensions.JSON, error) {
jsonByte, err := json.Marshal(document)
if err != nil {

View file

@ -2,7 +2,6 @@ package policy
import (
"encoding/json"
"strings"
"testing"
kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
@ -69,7 +68,7 @@ func Test_Validation_valid_backgroundPolicy(t *testing.T) {
err := json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsVariablesOtherThanObject(policy)
err = ValidateVariables(&policy, true)
assert.NilError(t, err)
}
@ -132,6 +131,6 @@ func Test_Validation_invalid_backgroundPolicy(t *testing.T) {
var policy kyverno.ClusterPolicy
err := json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsVariablesOtherThanObject(policy)
assert.Assert(t, strings.Contains(err.Error(), "variable serviceAccountName cannot be used, allowed variables: [request.object request.namespace images element mycm]"))
err = ValidateVariables(&policy, true)
assert.ErrorContains(t, err, "variable serviceAccountName must match")
}

View file

@ -185,7 +185,7 @@ func (pc *PolicyController) canBackgroundProcess(p *kyverno.ClusterPolicy) bool
return false
}
if err := ContainsVariablesOtherThanObject(*p); err != nil {
if err := ValidateVariables(p, true); err != nil {
logger.V(4).Info("policy cannot be processed in the background")
return false
}

View file

@ -7,6 +7,8 @@ import (
"regexp"
"strings"
"github.com/kyverno/kyverno/pkg/engine/context"
jsonpatch "github.com/evanphx/json-patch/v5"
"github.com/jmespath/go-jmespath"
kyverno "github.com/kyverno/kyverno/api/kyverno/v1"
@ -26,6 +28,13 @@ import (
"sigs.k8s.io/controller-runtime/pkg/log"
)
var allowedVariables = regexp.MustCompile(`request\.|serviceAccountName|serviceAccountNamespace|element\.|@|images\.|([a-z_0-9]+\()[^{}]`)
var allowedVariablesBackground = regexp.MustCompile(`request\.|element\.|@|images\.|([a-z_0-9]+\()[^{}]`)
// wildCardAllowedVariables represents regex for the allowed fields in wildcards
var wildCardAllowedVariables = regexp.MustCompile(`\{\{\s*(request\.|serviceAccountName|serviceAccountNamespace)[^{}]*\}\}`)
// validateJSONPatchPathForForwardSlash checks for forward slash
func validateJSONPatchPathForForwardSlash(patch string) error {
@ -64,37 +73,31 @@ func validateJSONPatchPathForForwardSlash(patch string) error {
// - One operation per rule
// - ResourceDescription mandatory checks
func Validate(policy *kyverno.ClusterPolicy, client *dclient.Client, mock bool, openAPIController *openapi.Controller) error {
p := *policy
namespacedPolicyBool := false
namespaced := false
background := policy.Spec.Background == nil || *policy.Spec.Background
clusterResources := make([]string, 0)
if len(common.PolicyHasVariables(p)) > 0 {
err := common.PolicyHasNonAllowedVariables(p)
if err != nil {
return fmt.Errorf("policy contains invalid variables: %s", err.Error())
}
err := ValidateVariables(policy, background)
if err != nil {
return err
}
// policy name is stored in the label of the report change request
if len(p.Name) > 63 {
return fmt.Errorf("invalid policy name %s: must be no more than 63 characters", p.Name)
if len(policy.Name) > 63 {
return fmt.Errorf("invalid policy name %s: must be no more than 63 characters", policy.Name)
}
if path, err := validateUniqueRuleName(p); err != nil {
if path, err := validateUniqueRuleName(*policy); err != nil {
return fmt.Errorf("path: spec.%s: %v", path, err)
}
if p.Spec.Background == nil || *p.Spec.Background {
if err := ContainsVariablesOtherThanObject(p); err != nil {
return fmt.Errorf("only select variables are allowed in background mode. Set spec.background=false to disable background mode for this policy rule: %s ", err)
}
}
if p.ObjectMeta.Namespace != "" {
namespacedPolicyBool = true
if policy.ObjectMeta.Namespace != "" {
namespaced = true
}
var res []*metav1.APIResourceList
if !mock && namespacedPolicyBool {
if !mock && namespaced {
var Empty struct{}
clusterResourcesMap := make(map[string]*struct{})
// Get all the cluster type kind supported by cluster
@ -118,7 +121,7 @@ func Validate(policy *kyverno.ClusterPolicy, client *dclient.Client, mock bool,
}
}
for i, rule := range p.Spec.Rules {
for i, rule := range policy.Spec.Rules {
//check for forward slash
if err := validateJSONPatchPathForForwardSlash(rule.Mutation.PatchesJSON6902); err != nil {
return fmt.Errorf("path must begin with a forward slash: spec.rules[%d]: %s", i, err)
@ -150,7 +153,7 @@ func Validate(policy *kyverno.ClusterPolicy, client *dclient.Client, mock bool,
// validate Cluster Resources in namespaced policy
// For namespaced policy, ClusterResource type field and values are not allowed in match and exclude
if namespacedPolicyBool {
if namespaced {
return checkClusterResourceInMatchAndExclude(rule, clusterResources, mock, res)
}
@ -162,7 +165,7 @@ func Validate(policy *kyverno.ClusterPolicy, client *dclient.Client, mock bool,
// - Mutate
// - Validate
// - Generate
if err := validateActions(i, &p.Spec.Rules[i], client, mock); err != nil {
if err := validateActions(i, &policy.Spec.Rules[i], client, mock); err != nil {
return err
}
@ -186,7 +189,7 @@ func Validate(policy *kyverno.ClusterPolicy, client *dclient.Client, mock bool,
}
}
if utils.ContainsString(rule.MatchResources.Kinds, "*") && (p.Spec.Background == nil || *p.Spec.Background) {
if utils.ContainsString(rule.MatchResources.Kinds, "*") && (policy.Spec.Background == nil || *policy.Spec.Background) {
return fmt.Errorf("wildcard policy not allowed in background mode. Set spec.background=false to disable background mode for this policy rule ")
}
@ -214,7 +217,7 @@ func Validate(policy *kyverno.ClusterPolicy, client *dclient.Client, mock bool,
switch typedConditions := kyvernoConditions.(type) {
case []kyverno.Condition: // backwards compatibility
for _, condition := range typedConditions {
if !strings.Contains(condition.Key.(string), "request.object.metadata.") && (!common.WildCardAllowedVariables.MatchString(condition.Key.(string)) || strings.Contains(condition.Key.(string), "request.object.spec")) {
if !strings.Contains(condition.Key.(string), "request.object.metadata.") && (!wildCardAllowedVariables.MatchString(condition.Key.(string)) || strings.Contains(condition.Key.(string), "request.object.spec")) {
return fmt.Errorf("policy can only deal with the metadata field of the resource if" +
" the rule does not match any kind")
}
@ -234,36 +237,36 @@ func Validate(policy *kyverno.ClusterPolicy, client *dclient.Client, mock bool,
match := rule.MatchResources
exclude := rule.ExcludeResources
for _, value := range match.Any {
err := validateKinds(value.ResourceDescription.Kinds, mock, client, p)
err := validateKinds(value.ResourceDescription.Kinds, mock, client, *policy)
if err != nil {
return fmt.Errorf("the kind defined in the any match resource is invalid")
}
}
for _, value := range match.All {
err := validateKinds(value.ResourceDescription.Kinds, mock, client, p)
err := validateKinds(value.ResourceDescription.Kinds, mock, client, *policy)
if err != nil {
return fmt.Errorf("the kind defined in the all match resource is invalid")
}
}
for _, value := range exclude.Any {
err := validateKinds(value.ResourceDescription.Kinds, mock, client, p)
err := validateKinds(value.ResourceDescription.Kinds, mock, client, *policy)
if err != nil {
return fmt.Errorf("the kind defined in the any exclude resource is invalid")
}
}
for _, value := range exclude.All {
err := validateKinds(value.ResourceDescription.Kinds, mock, client, p)
err := validateKinds(value.ResourceDescription.Kinds, mock, client, *policy)
if err != nil {
return fmt.Errorf("the kind defined in the all exclude resource is invalid")
}
}
if !utils.ContainsString(rule.MatchResources.Kinds, "*") {
err := validateKinds(rule.MatchResources.Kinds, mock, client, p)
err := validateKinds(rule.MatchResources.Kinds, mock, client, *policy)
if err != nil {
return errors.Wrapf(err, "match resource kind is invalid")
}
err = validateKinds(rule.ExcludeResources.Kinds, mock, client, p)
err = validateKinds(rule.ExcludeResources.Kinds, mock, client, *policy)
if err != nil {
return errors.Wrapf(err, "exclude resource kind is invalid")
}
@ -287,18 +290,18 @@ func Validate(policy *kyverno.ClusterPolicy, client *dclient.Client, mock bool,
if len(label) == 0 {
label = make(map[string]string)
label["generate.kyverno.io/clone-policy-name"] = p.GetName()
label["generate.kyverno.io/clone-policy-name"] = policy.GetName()
} else {
if label["generate.kyverno.io/clone-policy-name"] != "" {
policyNames := label["generate.kyverno.io/clone-policy-name"]
if !strings.Contains(policyNames, p.GetName()) {
policyNames = policyNames + "," + p.GetName()
if !strings.Contains(policyNames, policy.GetName()) {
policyNames = policyNames + "," + policy.GetName()
label["generate.kyverno.io/clone-policy-name"] = policyNames
} else {
updateSource = false
}
} else {
label["generate.kyverno.io/clone-policy-name"] = p.GetName()
label["generate.kyverno.io/clone-policy-name"] = policy.GetName()
}
}
@ -316,11 +319,11 @@ func Validate(policy *kyverno.ClusterPolicy, client *dclient.Client, mock bool,
}
if !mock {
if err := openAPIController.ValidatePolicyFields(p); err != nil {
if err := openAPIController.ValidatePolicyFields(*policy); err != nil {
return err
}
} else {
if err := openAPIController.ValidatePolicyMutation(p); err != nil {
if err := openAPIController.ValidatePolicyMutation(*policy); err != nil {
return err
}
}
@ -328,6 +331,171 @@ func Validate(policy *kyverno.ClusterPolicy, client *dclient.Client, mock bool,
return nil
}
func ValidateVariables(p *kyverno.ClusterPolicy, backgroundMode bool) error {
vars := hasVariables(p)
if len(vars) == 0 {
return nil
}
if err := hasInvalidVariables(p, backgroundMode); err != nil {
return fmt.Errorf("policy contains invalid variables: %s", err.Error())
}
if backgroundMode {
if err := containsUserVariables(p, vars); err != nil {
return fmt.Errorf("only select variables are allowed in background mode. Set spec.background=false to disable background mode for this policy rule: %s ", err)
}
}
return nil
}
// hasInvalidVariables - checks for unexpected variables in the policy
func hasInvalidVariables(policy *kyverno.ClusterPolicy, background bool) error {
for _, r := range policy.Spec.Rules {
ruleCopy := r.DeepCopy()
if err := ruleForbiddenSectionsHaveVariables(ruleCopy); err != nil {
return err
}
// skip variable checks on verifyImages.attestations, as variables in attestations are dynamic
for _, vi := range ruleCopy.VerifyImages {
for _, a := range vi.Attestations {
a.Conditions = nil
}
}
ctx := buildContext(ruleCopy, background)
if _, err := variables.SubstituteAllInRule(log.Log, ctx, *ruleCopy); !checkNotFoundErr(err) {
return fmt.Errorf("variable substitution failed for rule %s: %s", ruleCopy.Name, err.Error())
}
}
return nil
}
// for now forbidden sections are match, exclude and
func ruleForbiddenSectionsHaveVariables(rule *kyverno.Rule) error {
var err error
err = jsonPatchPathHasVariables(rule.Mutation.PatchesJSON6902)
if err != nil {
return fmt.Errorf("rule \"%s\" should not have variables in patchesJSON6902 path section", rule.Name)
}
err = objectHasVariables(rule.ExcludeResources)
if err != nil {
return fmt.Errorf("rule \"%s\" should not have variables in exclude section", rule.Name)
}
err = objectHasVariables(rule.MatchResources)
if err != nil {
return fmt.Errorf("rule \"%s\" should not have variables in match section", rule.Name)
}
return nil
}
// hasVariables - check for variables in the policy
func hasVariables(policy *kyverno.ClusterPolicy) [][]string {
policyRaw, _ := json.Marshal(policy)
matches := variables.RegexVariables.FindAllStringSubmatch(string(policyRaw), -1)
return matches
}
func jsonPatchPathHasVariables(patch string) error {
jsonPatch, err := yaml.ToJSON([]byte(patch))
if err != nil {
return err
}
decodedPatch, err := jsonpatch.DecodePatch(jsonPatch)
if err != nil {
return err
}
for _, operation := range decodedPatch {
path, err := operation.Path()
if err != nil {
return err
}
vars := variables.RegexVariables.FindAllString(path, -1)
if len(vars) > 0 {
return fmt.Errorf("operation \"%s\" has forbidden variables", operation.Kind())
}
}
return nil
}
func objectHasVariables(object interface{}) error {
var err error
objectJSON, err := json.Marshal(object)
if err != nil {
return err
}
if len(common.RegexVariables.FindAllStringSubmatch(string(objectJSON), -1)) > 0 {
return fmt.Errorf("invalid variables")
}
return nil
}
func buildContext(rule *kyverno.Rule, background bool) *context.MockContext {
re := getAllowedVariables(background)
ctx := context.NewMockContext(re)
addContextVariables(rule.Context, ctx)
for _, fe := range rule.Validation.ForEachValidation {
addContextVariables(fe.Context, ctx)
}
for _, fe := range rule.Mutation.ForEachMutation {
addContextVariables(fe.Context, ctx)
}
return ctx
}
func getAllowedVariables(background bool) *regexp.Regexp {
if background {
return allowedVariablesBackground
}
return allowedVariables
}
func addContextVariables(entries []kyverno.ContextEntry, ctx *context.MockContext) {
for _, contextEntry := range entries {
if contextEntry.APICall != nil {
ctx.AddVariable(contextEntry.Name + "*")
}
if contextEntry.ConfigMap != nil {
ctx.AddVariable(contextEntry.Name + ".data.*")
}
}
}
func checkNotFoundErr(err error) bool {
if err != nil {
switch err.(type) {
case jmespath.NotFoundError:
return true
case context.InvalidVariableErr:
return false
default:
return false
}
}
return true
}
func validateElementInForEach(document apiextensions.JSON) error {
jsonByte, err := json.Marshal(document)
if err != nil {
@ -348,6 +516,7 @@ func validateMatchKindHelper(rule kyverno.Rule) error {
return fmt.Errorf("policy can only deal with the metadata field of the resource if" +
" the rule does not match any kind")
}
return fmt.Errorf("at least one element must be specified in a kind block, the kind attribute is mandatory when working with the resources element")
}

View file

@ -833,7 +833,7 @@ func Test_BackGroundUserInfo_match_roles(t *testing.T) {
err = json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsVariablesOtherThanObject(*policy)
err = containsUserVariables(policy, nil)
assert.Equal(t, err.Error(), "invalid variable used at path: spec/rules[0]/match/roles")
}
@ -865,8 +865,7 @@ func Test_BackGroundUserInfo_match_clusterRoles(t *testing.T) {
err = json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsVariablesOtherThanObject(*policy)
err = containsUserVariables(policy, nil)
assert.Equal(t, err.Error(), "invalid variable used at path: spec/rules[0]/match/clusterRoles")
}
@ -901,8 +900,7 @@ func Test_BackGroundUserInfo_match_subjects(t *testing.T) {
err = json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsVariablesOtherThanObject(*policy)
err = containsUserVariables(policy, nil)
assert.Equal(t, err.Error(), "invalid variable used at path: spec/rules[0]/match/subjects")
}
@ -933,7 +931,7 @@ func Test_BackGroundUserInfo_mutate_overlay1(t *testing.T) {
err = json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsVariablesOtherThanObject(*policy)
err = ValidateVariables(policy, true)
assert.Assert(t, err != nil)
}
@ -964,7 +962,7 @@ func Test_BackGroundUserInfo_mutate_overlay2(t *testing.T) {
err = json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsVariablesOtherThanObject(*policy)
err = ValidateVariables(policy, true)
assert.Assert(t, err != nil)
}
@ -995,7 +993,7 @@ func Test_BackGroundUserInfo_validate_pattern(t *testing.T) {
err = json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsVariablesOtherThanObject(*policy)
err = ValidateVariables(policy, true)
assert.Assert(t, err != nil, err)
}
@ -1030,7 +1028,7 @@ func Test_BackGroundUserInfo_validate_anyPattern(t *testing.T) {
err = json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsVariablesOtherThanObject(*policy)
err = ValidateVariables(policy, true)
assert.Assert(t, err != nil)
}
@ -1065,7 +1063,7 @@ func Test_BackGroundUserInfo_validate_anyPattern_multiple_var(t *testing.T) {
err = json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsVariablesOtherThanObject(*policy)
err = ValidateVariables(policy, true)
assert.Assert(t, err != nil)
}
@ -1100,7 +1098,7 @@ func Test_BackGroundUserInfo_validate_anyPattern_serviceAccount(t *testing.T) {
err = json.Unmarshal(rawPolicy, &policy)
assert.NilError(t, err)
err = ContainsVariablesOtherThanObject(*policy)
err = ValidateVariables(policy, true)
assert.Assert(t, err != nil)
}