mirror of
https://github.com/kyverno/kyverno.git
synced 2025-04-08 10:04:25 +00:00
redo variable validation (#2647)
* 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:
parent
40d30df726
commit
5c16ee738a
15 changed files with 416 additions and 401 deletions
pkg
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
101
pkg/engine/context/mock_context.go
Normal file
101
pkg/engine/context/mock_context.go
Normal 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
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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"))
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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)://")
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
|
@ -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 {
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
||||
|
|
Loading…
Add table
Reference in a new issue