1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2024-12-14 11:57:48 +00:00

Bug fixes - policy validation, auto-generated rules, apiCall support in mutate and generate (#1629)

* Fix invalid policy reports generated for blocked resource

Signed-off-by: Shuting Zhao <shutting06@gmail.com>

* fix 1464 - copy context and preconditions to auto-gen rules

Signed-off-by: Shuting Zhao <shutting06@gmail.com>

* fix 1628 - add policy validations

Signed-off-by: Shuting Zhao <shutting06@gmail.com>

* fix 1593 - support apiCall in mutate and generate

Signed-off-by: Shuting Zhao <shutting06@gmail.com>

* fix test

Signed-off-by: Shuting Zhao <shutting06@gmail.com>
This commit is contained in:
shuting 2021-02-22 12:08:26 -08:00 committed by GitHub
parent 6fc349716c
commit 267be0815f
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 154 additions and 33 deletions

View file

@ -68,6 +68,7 @@ docker-build-kyverno:
docker-build-local-kyverno:
CGO_ENABLED=0 GOOS=linux go build -o $(PWD)/$(KYVERNO_PATH)/kyverno -ldflags=$(LD_FLAGS) $(PWD)/$(KYVERNO_PATH)/main.go
@docker build -f $(PWD)/$(KYVERNO_PATH)/localDockerfile -t $(REPO)/$(KYVERNO_IMAGE):$(IMAGE_TAG) $(PWD)/$(KYVERNO_PATH)
@docker tag $(REPO)/$(KYVERNO_IMAGE):$(IMAGE_TAG) $(REPO)/$(KYVERNO_IMAGE):latest
docker-build-kyverno-amd64:
@docker build -f $(PWD)/$(KYVERNO_PATH)/Dockerfile -t $(REPO)/$(KYVERNO_IMAGE):$(IMAGE_TAG) . --build-arg LD_FLAGS=$(LD_FLAGS) --build-arg TARGETPLATFORM="linux/amd64"

View file

@ -2,14 +2,14 @@ package context
import (
"encoding/json"
"fmt"
"strings"
"sync"
"k8s.io/api/admission/v1beta1"
jsonpatch "github.com/evanphx/json-patch"
"github.com/go-logr/logr"
kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1"
"k8s.io/api/admission/v1beta1"
"sigs.k8s.io/controller-runtime/pkg/log"
)
@ -57,6 +57,16 @@ 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
@ -71,7 +81,7 @@ func (ctx *Context) AddJSON(dataRaw []byte) error {
return nil
}
// AddRequest addes an admission request to context
// AddRequest adds an admission request to context
func (ctx *Context) AddRequest(request *v1beta1.AdmissionRequest) error {
modifiedResource := struct {
Request interface{} `json:"request"`
@ -90,10 +100,10 @@ func (ctx *Context) AddRequest(request *v1beta1.AdmissionRequest) error {
//AddResource data at path: request.object
func (ctx *Context) AddResource(dataRaw []byte) error {
// unmarshall the resource struct
// unmarshal the resource struct
var data interface{}
if err := json.Unmarshal(dataRaw, &data); err != nil {
ctx.log.Error(err, "failed to unmarshall the resource")
ctx.log.Error(err, "failed to unmarshal the resource")
return err
}
@ -203,3 +213,20 @@ func (ctx *Context) Restore() {
ctx.jsonRaw = make([]byte, len(ctx.jsonRawCheckpoint))
copy(ctx.jsonRaw, ctx.jsonRawCheckpoint)
}
// 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

@ -18,7 +18,10 @@ func (ctx *Context) Query(query string) (interface{}, error) {
var emptyResult interface{}
// check for white-listed variables
if !ctx.isBuiltInVariable(query) {
return emptyResult, fmt.Errorf("variable %s cannot be used", query)
return emptyResult, InvalidVariableErr{
variable: query,
whiteList: ctx.getBuiltInVars(),
}
}
// compile the query
@ -34,7 +37,7 @@ func (ctx *Context) Query(query string) (interface{}, error) {
var data interface{}
if err := json.Unmarshal(ctx.jsonRaw, &data); err != nil {
ctx.log.Error(err, "failed to unmarshal context")
return emptyResult, fmt.Errorf("failed to unmarshall context: %v", err)
return emptyResult, fmt.Errorf("failed to unmarshal context: %v", err)
}
result, err := queryPath.Search(data)
@ -46,10 +49,10 @@ func (ctx *Context) Query(query string) (interface{}, error) {
}
func (ctx *Context) isBuiltInVariable(variable string) bool {
if len(ctx.builtInVars) == 0 {
if len(ctx.getBuiltInVars()) == 0 {
return true
}
for _, wVar := range ctx.builtInVars {
for _, wVar := range ctx.getBuiltInVars() {
if strings.HasPrefix(variable, wVar) {
return true
}

View file

@ -86,9 +86,8 @@ func filterRule(rule kyverno.Rule, policyContext *PolicyContext) *response.RuleR
policyContext.JSONContext.Checkpoint()
defer policyContext.JSONContext.Restore()
// add configmap json data to context
if err := LoadContext(logger, rule.Context, resCache, policyContext); err != nil {
logger.V(4).Info("cannot add configmaps to context", "reason", err.Error())
logger.V(4).Info("cannot add external data to the context", "reason", err.Error())
return nil
}

View file

@ -4,6 +4,7 @@ import (
"encoding/json"
"errors"
"fmt"
"github.com/go-logr/logr"
"github.com/jmespath/go-jmespath"
kyverno "github.com/kyverno/kyverno/pkg/api/kyverno/v1"

View file

@ -58,7 +58,7 @@ func TestTypeConversion(t *testing.T) {
[]byte(`{"path":"/spec/template/spec/containers/0/name","op":"replace","value":"my-nginx"}`),
}
// serilize resource
// serialize resource
inputJSONgo, err := yaml.YAMLToJSON(inputBytes)
assert.Nil(t, err)

View file

@ -71,7 +71,7 @@ func Mutate(policyContext *PolicyContext) (resp *response.EngineResponse) {
policyContext.JSONContext.Restore()
if err := LoadContext(logger, rule.Context, resCache, policyContext); err != nil {
logger.V(2).Info("failed to load context", "reason", err.Error())
logger.Error(err, "failed to load context")
continue
}

View file

@ -106,7 +106,7 @@ func validateResource(log logr.Logger, ctx *PolicyContext) *response.EngineRespo
ctx.JSONContext.Restore()
if err := LoadContext(log, rule.Context, ctx.ResourceCache, ctx); err != nil {
log.V(2).Info("failed to load context", "reason", err.Error())
log.Error(err, "failed to load context")
continue
}

View file

@ -96,7 +96,12 @@ func subValR(log logr.Logger, ctx context.EvalInterface, valuePattern string, pa
variable = strings.TrimSpace(variable)
substitutedVar, err := ctx.Query(variable)
if err != nil {
return nil, fmt.Errorf("failed to resolve %v at path %s", variable, path)
switch err.(type) {
case context.InvalidVariableErr:
return nil, err
default:
return nil, fmt.Errorf("failed to resolve %v at path %s", variable, path)
}
}
log.V(3).Info("variable substituted", "variable", v, "value", substitutedVar, "path", path)

View file

@ -202,3 +202,27 @@ func Test_SubstituteRecursive(t *testing.T) {
t.Errorf("expected %s received %v", "temp", results)
}
}
func Test_policyContextValidation(t *testing.T) {
policyContext := []byte(`
{
"context": [
{
"name": "myconfigmap",
"apiCall": {
"urlPath": "/api/v1/namespaces/{{ request.namespace }}/configmaps/generate-pod"
}
}
]
}
`)
var contextMap interface{}
err := json.Unmarshal(policyContext, &contextMap)
assert.NilError(t, err)
ctx := context.NewContext("request.object")
_, err = SubstituteVars(log.Log, ctx, contextMap)
assert.Assert(t, err != nil, err)
}

View file

@ -132,6 +132,7 @@ func (c *Controller) applyGenerate(resource unstructured.Unstructured, gr kyvern
ResourceCache: c.resCache,
JSONContext: ctx,
NamespaceLabels: namespaceLabels,
Client: c.client,
}
// check if the policy still applies to the resource

View file

@ -20,20 +20,43 @@ func ContainsVariablesOtherThanObject(policy kyverno.ClusterPolicy) error {
if path := userInfoDefined(rule.ExcludeResources.UserInfo); path != "" {
return fmt.Errorf("invalid variable used at path: spec/rules[%d]/exclude/%s", idx, path)
}
// Skip Validation if rule contains Context
if len(rule.Context) > 0 {
return nil
}
filterVars := []string{"request.object"}
ctx := context.NewContext(filterVars...)
for contextIdx, contextEntry := range rule.Context {
if contextEntry.APICall != nil {
ctx.AddBuiltInVars(contextEntry.Name)
if _, err := variables.SubstituteVars(log.Log, ctx, contextEntry.APICall.URLPath); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/context[%d]/apiCall/urlPath: %s", idx, contextIdx, err.Error())
}
if _, err := variables.SubstituteVars(log.Log, ctx, contextEntry.APICall.JMESPath); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/context[%d]/apiCall/jmesPath: %s", idx, contextIdx, err.Error())
}
}
if contextEntry.ConfigMap != nil {
ctx.AddBuiltInVars(contextEntry.Name)
if _, err = variables.SubstituteVars(log.Log, ctx, contextEntry.ConfigMap.Name); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/context[%d]/configMap/name", idx, contextIdx)
}
if _, err = variables.SubstituteVars(log.Log, ctx, contextEntry.ConfigMap.Namespace); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/context[%d]/configMap/namespace", idx, contextIdx)
}
}
}
for condIdx, condition := range rule.Conditions {
if condition.Key, err = variables.SubstituteVars(log.Log, ctx, condition.Key); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable %s used at spec/rules[%d]/condition[%d]/key", condition.Key, idx, condIdx)
return fmt.Errorf("invalid variable %v used at spec/rules[%d]/condition[%d]/key", condition.Key, idx, condIdx)
}
if condition.Value, err = variables.SubstituteVars(log.Log, ctx, condition.Value); !checkNotFoundErr(err) {
return fmt.Errorf("invalid %s variable used at spec/rules[%d]/condition[%d]/value", condition.Value, idx, condIdx)
return fmt.Errorf("invalid %v variable used at spec/rules[%d]/condition[%d]/value: %v", condition.Value, idx, condIdx, err)
}
}
@ -43,9 +66,15 @@ func ContainsVariablesOtherThanObject(policy kyverno.ClusterPolicy) error {
}
}
if rule.Mutation.PatchStrategicMerge != nil {
if rule.Mutation.Overlay, err = variables.SubstituteVars(log.Log, ctx, rule.Mutation.PatchStrategicMerge); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/mutate/patchStrategicMerge", idx)
}
}
if rule.Validation.Pattern != nil {
if rule.Validation.Pattern, err = variables.SubstituteVars(log.Log, ctx, rule.Validation.Pattern); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/validate/pattern", idx)
return fmt.Errorf("invalid variable used at spec/rules[%d]/validate/pattern: %v", idx, err)
}
}
@ -76,6 +105,26 @@ func ContainsVariablesOtherThanObject(policy kyverno.ClusterPolicy) error {
}
}
}
if _, err = variables.SubstituteVars(log.Log, ctx, rule.Generation.Name); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/generate/name: %v", idx, err)
}
if _, err = variables.SubstituteVars(log.Log, ctx, rule.Generation.Namespace); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/generate/name: %v", idx, err)
}
if _, err = variables.SubstituteVars(log.Log, ctx, rule.Generation.Data); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/generate/data: %v", idx, err)
}
if _, err = variables.SubstituteVars(log.Log, ctx, rule.Generation.Clone.Name); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/generate/clone/name: %v", idx, err)
}
if _, err = variables.SubstituteVars(log.Log, ctx, rule.Generation.Clone.Namespace); !checkNotFoundErr(err) {
return fmt.Errorf("invalid variable used at spec/rules[%d]/generate/clone/namespace: %v", idx, err)
}
}
return nil
@ -86,6 +135,9 @@ func checkNotFoundErr(err error) bool {
switch err.(type) {
case variables.NotFoundVariableErr:
return true
case context.InvalidVariableErr:
// non-white-listed variable is found
return false
default:
return false
}

View file

@ -65,7 +65,7 @@ func (g *Generate) Validate() (string, error) {
// Kyverno generate-controller create/update/deletes the resources specified in generate rule of policy
// kyverno uses SA 'kyverno-service-account' and has default ClusterRoles and ClusterRoleBindings
// instuctions to modify the RBAC for kyverno are mentioned at https://github.com/kyverno/kyverno/blob/master/documentation/installation.md
// instructions to modify the RBAC for kyverno are mentioned at https://github.com/kyverno/kyverno/blob/master/documentation/installation.md
// - operations required: create/update/delete/get
// If kind and namespace contain variables, then we cannot resolve then so we skip the processing
if err := g.canIGenerate(kind, namespace); err != nil {
@ -96,7 +96,7 @@ func (g *Generate) validateClone(c kyverno.CloneFrom, kind string) (string, erro
return "", nil
}
//canIGenerate returns a error if kyverno cannot perform oprations
//canIGenerate returns a error if kyverno cannot perform operations
func (g *Generate) canIGenerate(kind, namespace string) error {
// Skip if there is variable defined
authCheck := g.authCheck

View file

@ -40,7 +40,7 @@ func Validate(policy *kyverno.ClusterPolicy, client *dclient.Client, mock bool,
}
if p.Spec.Background == nil || *p.Spec.Background == true {
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)
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)
}
}

View file

@ -983,11 +983,7 @@ func Test_BackGroundUserInfo_validate_pattern(t *testing.T) {
assert.NilError(t, err)
err = ContainsVariablesOtherThanObject(*policy)
if err.Error() != "invalid variable used at spec/rules[0]/validate/pattern" {
t.Log(err)
t.Error("Incorrect Path")
}
assert.Assert(t, err != nil, err)
}
func Test_BackGroundUserInfo_validate_anyPattern(t *testing.T) {

View file

@ -359,6 +359,8 @@ type kyvernoRule struct {
Name string `json:"name"`
MatchResources *kyverno.MatchResources `json:"match"`
ExcludeResources *kyverno.ExcludeResources `json:"exclude,omitempty"`
Context *[]kyverno.ContextEntry `json:"context,omitempty"`
Conditions *[]kyverno.Condition `json:"preconditions,omitempty"`
Mutation *kyverno.Mutation `json:"mutate,omitempty"`
Validation *kyverno.Validation `json:"validate,omitempty"`
}
@ -423,6 +425,14 @@ func generateRuleForControllers(rule kyverno.Rule, controllers string, log logr.
MatchResources: match.DeepCopy(),
}
if len(rule.Context) > 0 {
controllerRule.Context = &rule.DeepCopy().Context
}
if len(rule.Conditions) > 0 {
controllerRule.Conditions = &rule.DeepCopy().Conditions
}
if !reflect.DeepEqual(exclude, kyverno.ExcludeResources{}) {
controllerRule.ExcludeResources = exclude.DeepCopy()
}

View file

@ -51,6 +51,7 @@ func (ws *WebhookServer) HandleGenerate(request *v1beta1.AdmissionRequest, polic
ExcludeResourceFunc: ws.configHandler.ToFilter,
ResourceCache: ws.resCache,
JSONContext: ctx,
Client: ws.client,
}
for _, policy := range policies {

View file

@ -45,6 +45,7 @@ func (ws *WebhookServer) HandleMutation(
ExcludeResourceFunc: ws.configHandler.ToFilter,
ResourceCache: ws.resCache,
JSONContext: ctx,
Client: ws.client,
}
if request.Operation == v1beta1.Update {

View file

@ -417,14 +417,14 @@ func (ws *WebhookServer) resourceValidation(request *v1beta1.AdmissionRequest) *
logger.V(6).Info("received an admission request in validating webhook")
// push admission request to audit handler, this won't block the admission request
ws.auditHandler.Add(request.DeepCopy())
policies := ws.pCache.Get(policycache.ValidateEnforce, nil)
// Get namespace policies from the cache for the requested resource namespace
nsPolicies := ws.pCache.Get(policycache.ValidateEnforce, &request.Namespace)
policies = append(policies, nsPolicies...)
if len(policies) == 0 {
// push admission request to audit handler, this won't block the admission request
ws.auditHandler.Add(request.DeepCopy())
logger.V(4).Info("no enforce validation policies; returning AdmissionResponse.Allowed: true")
return &v1beta1.AdmissionResponse{Allowed: true}
}