1
0
Fork 0
mirror of https://github.com/kyverno/kyverno.git synced 2025-04-09 18:52:36 +00:00

fix deny rules

Signed-off-by: Jim Bugwadia <jim@nirmata.com>
This commit is contained in:
Jim Bugwadia 2021-09-27 14:28:55 -07:00
parent 536b64bed1
commit a905a61581
10 changed files with 431 additions and 33 deletions

View file

@ -1,5 +1,7 @@
package common
import "encoding/json"
// CopyMap creates a full copy of the target map
func CopyMap(m map[string]interface{}) map[string]interface{} {
mapCopy := make(map[string]interface{})
@ -17,3 +19,22 @@ func CopySlice(s []interface{}) []interface{} {
return sliceCopy
}
func ToMap(data interface{}) (map[string]interface{}, error) {
if m, ok := data.(map[string]interface{}); ok {
return m, nil
}
b, err := json.Marshal(data)
if err != nil {
return nil, err
}
mapData := make(map[string]interface{})
err = json.Unmarshal(b, &mapData)
if err != nil {
return nil, err
}
return mapData, nil
}

View file

@ -188,6 +188,7 @@ func (ctx *Context) AddResourceAsObject(data interface{}) error {
ctx.log.Error(err, "failed to marshal the resource")
return err
}
return ctx.AddJSON(objRaw)
}

View file

@ -117,7 +117,7 @@ func Mutate(policyContext *PolicyContext) (resp *response.EngineResponse) {
ruleResp := response.RuleResponse{
Name: ruleCopy.Name,
Type: utils.Validation.String(),
Message: fmt.Sprintf("variable substitution failed for rule %s: %s", ruleCopy.Name, err.Error()),
Message: fmt.Sprintf("variable substitution failed: %s", err.Error()),
Status: response.RuleStatusPass,
}

View file

@ -157,7 +157,7 @@ func Test_variableSubstitutionPathNotExist(t *testing.T) {
JSONContext: ctx,
NewResource: *resourceUnstructured}
er := Mutate(policyContext)
expectedErrorStr := "variable substitution failed for rule test-path-not-exist: Unknown key \"name1\" in path"
expectedErrorStr := "variable substitution failed: Unknown key \"name1\" in path"
assert.Equal(t, er.PolicyResponse.Rules[0].Message, expectedErrorStr)
}

View file

@ -40,3 +40,18 @@ type PolicyContext struct {
// NamespaceLabels stores the label of namespace to be processed by namespace selector
NamespaceLabels map[string]string
}
func (pc *PolicyContext) Copy() *PolicyContext {
return &PolicyContext{
Policy: pc.Policy,
NewResource: pc.NewResource,
OldResource: pc.OldResource,
AdmissionInfo: pc.AdmissionInfo,
Client: pc.Client,
ExcludeGroupRole: pc.ExcludeGroupRole,
ExcludeResourceFunc: pc.ExcludeResourceFunc,
ResourceCache: pc.ResourceCache,
JSONContext: pc.JSONContext,
NamespaceLabels: pc.NamespaceLabels,
}
}

View file

@ -19,11 +19,13 @@ func MatchPattern(logger logr.Logger, resource, pattern interface{}) (error, str
ac := common.NewAnchorMap()
elemPath, err := validateResourceElement(logger, resource, pattern, pattern, "/", ac)
if err != nil {
// if conditional or global anchors report errors, the rule does not apply to the resource
if common.IsConditionalAnchorError(err.Error()) || common.IsGlobalAnchorError(err.Error()) {
logger.V(3).Info(ac.AnchorError.Message)
return ac.AnchorError.Error(), ""
logger.V(3).Info("skipping resource as anchor does not apply", "msg", ac.AnchorError.Error())
return nil, ""
}
// check if an anchor defined in the policy rule is missing in the resource
if ac.IsAnchorError() {
logger.V(3).Info("missing anchor in resource")
return err, ""

View file

@ -1506,7 +1506,7 @@ func TestConditionalAnchorWithMultiplePatterns(t *testing.T) {
}
for _, testCase := range testCases {
executeTestCase(t, testCase)
testMatchPattern(t, testCase)
}
}
@ -1518,17 +1518,24 @@ func Test_global_anchor(t *testing.T) {
nilErr bool
} {
{
name: "check global anchor",
name: "check global anchor_skip",
pattern: []byte(`{"spec": {"containers": [{"name": "*","<(image)": "*:latest","imagePullPolicy": "!Always"}]}}`),
resource: []byte(`{"spec": {"containers": [{"name": "nginx","image": "nginx", "imagePullPolicy": "Always"}]}}`),
resource: []byte(`{"spec": {"containers": [{"name": "nginx","image": "nginx:v1", "imagePullPolicy": "Always"}]}}`),
nilErr: true,
},
{
name: "check global anchor_apply",
pattern: []byte(`{"spec": {"containers": [{"name": "*","<(image)": "*:latest","imagePullPolicy": "!Always"}]}}`),
resource: []byte(`{"spec": {"containers": [{"name": "nginx","image": "nginx:latest", "imagePullPolicy": "Always"}]}}`),
nilErr: false,
},
}
executeTestCase(t, testCases[0])
testMatchPattern(t, testCases[0])
testMatchPattern(t, testCases[1])
}
func executeTestCase(t *testing.T, testCase struct {name string;pattern []byte;resource []byte;nilErr bool}) {
func testMatchPattern(t *testing.T, testCase struct {name string;pattern []byte;resource []byte;nilErr bool}) {
var pattern, resource interface{}
err := json.Unmarshal(testCase.pattern, &pattern)
assert.NilError(t, err)
@ -1543,4 +1550,4 @@ func executeTestCase(t *testing.T, testCase struct {name string;pattern []b
err != nil,
fmt.Sprintf("\ntest: %s\npattern: %s\nresource: %s\nmsg: %v", testCase.name, pattern, resource, err))
}
}
}

View file

@ -3,6 +3,7 @@ package engine
import (
"encoding/json"
"fmt"
"github.com/kyverno/kyverno/pkg/engine/common"
"github.com/pkg/errors"
"k8s.io/apiextensions-apiserver/pkg/apis/apiextensions"
"reflect"
@ -189,7 +190,7 @@ func (v *validator) validate() *response.RuleResponse {
return ruleResponse(v.rule, "", response.RuleStatusSkip)
}
if v.rule.Validation.Pattern != nil || v.rule.Validation.AnyPattern != nil {
if v.pattern != nil || v.anyPattern != nil {
if err = v.substitutePatterns(); err != nil {
return ruleError(v.rule, "variable substitution failed", err)
}
@ -197,7 +198,7 @@ func (v *validator) validate() *response.RuleResponse {
ruleResponse := v.validateResourceWithRule()
return ruleResponse
} else if v.rule.Validation.Deny != nil {
} else if v.deny != nil {
ruleResponse := v.validateDeny()
return ruleResponse
}
@ -235,27 +236,46 @@ func (v *validator) validateForEach() *response.RuleResponse {
for _, e := range elements {
v.ctx.JSONContext.Reset()
elementData := make(map[string]interface{})
elementData["element"] = e
jsonData, err := json.Marshal(elementData)
if err != nil {
return ruleError(v.rule, fmt.Sprintf("failed to marshall element %v", e), err)
ctx := v.ctx.Copy()
if err := addElementToContext(ctx, e); err != nil {
v.log.Error(err, "failed to add element to context")
return ruleError(v.rule, "failed to process foreach", err)
}
if err := v.ctx.JSONContext.AddJSON(jsonData); err != nil {
return ruleError(v.rule, fmt.Sprintf("failed add element (%s) to context", string(jsonData)), err)
}
foreachValidator := newForeachValidator(v.log, v.ctx, v.rule)
foreachValidator := newForeachValidator(v.log, ctx, v.rule)
r := foreachValidator.validate()
if r.Status != response.RuleStatusPass {
if r == nil {
v.log.Info("skipping rule due to empty result")
} else if r.Status == response.RuleStatusSkip {
v.log.Info("skipping rule as preconditions were not met")
} else if r.Status != response.RuleStatusPass {
msg := fmt.Sprintf("validation failed in foreach rule for %v", e)
return ruleResponse(v.rule, msg, r.Status)
}
}
return ruleResponse(v.rule, "", response.RuleStatusPass)
}
func addElementToContext(ctx *PolicyContext, e interface{}) error {
data, err := common.ToMap(e)
if err != nil {
return err
}
u := unstructured.Unstructured{}
u.SetUnstructuredContent(data)
ctx.NewResource = u
if err := ctx.JSONContext.AddResourceAsObject(e); err != nil {
return errors.Wrapf(err, "failed to add resource (%v) to JSON context", e)
}
return nil
}
func (v *validator) evaluateList(jmesPath string) ([]interface{}, error) {
i, err := v.ctx.JSONContext.Query(jmesPath)
if err != nil {
@ -302,9 +322,9 @@ func (v *validator) checkPreconditions() (bool, error) {
func (v *validator) validateDeny() *response.RuleResponse {
anyAllCond := v.deny.AnyAllConditions
anyAllCond, err := variables.SubstituteAllInPreconditions(v.log, v.ctx.JSONContext, anyAllCond)
anyAllCond, err := variables.SubstituteAll(v.log, v.ctx.JSONContext, anyAllCond)
if err != nil {
return ruleError(v.rule, "failed to substitute variables in preconditions", err)
return ruleError(v.rule, "failed to substitute variables in deny conditions", err)
}
if err = v.substituteDeny(); err != nil {
@ -557,7 +577,7 @@ func (v *validator) substituteDeny() error {
}
func ruleError(rule *kyverno.Rule, msg string, err error) *response.RuleResponse {
msg = fmt.Sprintf("%s for rule %s: %s", msg, rule.Name, err.Error())
msg = fmt.Sprintf("%s: %s", msg, err.Error())
return ruleResponse(rule, msg, response.RuleStatusError)
}

View file

@ -1479,7 +1479,7 @@ func Test_VariableSubstitutionPathNotExistInPattern(t *testing.T) {
er := Validate(policyContext)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusError)
assert.Equal(t, er.PolicyResponse.Rules[0].Message,
"variable substitution failed for rule test-path-not-exist: Unknown key \"name1\" in path")
"variable substitution failed: Unknown key \"name1\" in path")
}
func Test_VariableSubstitutionPathNotExistInAnyPattern_OnePatternStatisfiesButSubstitutionFails(t *testing.T) {
@ -1570,7 +1570,7 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_OnePatternStatisfiesButSu
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusError)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "variable substitution failed for rule test-path-not-exist: Unknown key \"name1\" in path")
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "variable substitution failed: Unknown key \"name1\" in path")
}
func Test_VariableSubstitution_NotOperatorWithStringVariable(t *testing.T) {
@ -1720,7 +1720,7 @@ func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathNotPresent(t *test
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, response.RuleStatusError)
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "variable substitution failed for rule test-path-not-exist: Unknown key \"name1\" in path")
assert.Equal(t, er.PolicyResponse.Rules[0].Message, "variable substitution failed: Unknown key \"name1\" in path")
}
func Test_VariableSubstitutionPathNotExistInAnyPattern_AllPathPresent_NonePatternSatisfy(t *testing.T) {
@ -1932,8 +1932,8 @@ func Test_Flux_Kustomization_PathNotPresent(t *testing.T) {
policyRaw: []byte(`{"apiVersion":"kyverno.io/v1","kind":"ClusterPolicy","metadata":{"name":"flux-multi-tenancy"},"spec":{"validationFailureAction":"enforce","rules":[{"name":"serviceAccountName","exclude":{"resources":{"namespaces":["flux-system"]}},"match":{"resources":{"kinds":["Kustomization","HelmRelease"]}},"validate":{"message":".spec.serviceAccountName is required","pattern":{"spec":{"serviceAccountName":"?*"}}}},{"name":"sourceRefNamespace","exclude":{"resources":{"namespaces":["flux-system"]}},"match":{"resources":{"kinds":["Kustomization","HelmRelease"]}},"validate":{"message":"spec.sourceRef.namespace must be the same as metadata.namespace","deny":{"conditions":[{"key":"{{request.object.spec.sourceRef.namespace}}","operator":"NotEquals","value":"{{request.object.metadata.namespace}}"}]}}}]}}`),
// referred variable path not present
resourceRaw: []byte(`{"apiVersion":"kustomize.toolkit.fluxcd.io/v1beta1","kind":"Kustomization","metadata":{"name":"dev-team","namespace":"apps"},"spec":{"serviceAccountName":"dev-team","interval":"5m","sourceRef":{"kind":"GitRepository","name":"dev-team"},"prune":true,"validation":"client"}}`),
expectedResults: []response.RuleStatus{response.RuleStatusPass, response.RuleStatusFail},
expectedMessages: []string{"validation rule 'serviceAccountName' passed.", "spec.sourceRef.namespace must be the same as metadata.namespace"},
expectedResults: []response.RuleStatus{response.RuleStatusPass, response.RuleStatusError},
expectedMessages: []string{"validation rule 'serviceAccountName' passed.", "failed to substitute variables in deny conditions: Unknown key \"namespace\" in path"},
},
{
name: "resource-with-violation",
@ -1970,7 +1970,7 @@ func Test_Flux_Kustomization_PathNotPresent(t *testing.T) {
er := Validate(policyContext)
for i, rule := range er.PolicyResponse.Rules {
assert.Equal(t, er.PolicyResponse.Rules[i].Status, test.expectedResults[i])
assert.Equal(t, er.PolicyResponse.Rules[i].Status, test.expectedResults[i], "\ntest %s failed\nexpected: %s\nactual: %s", test.name, test.expectedResults[i].String(), er.PolicyResponse.Rules[i].Status.String())
assert.Equal(t, er.PolicyResponse.Rules[i].Message, test.expectedMessages[i], "\ntest %s failed\nexpected: %s\nactual: %s", test.name, test.expectedMessages[i], rule.Message)
}
}
@ -2418,3 +2418,335 @@ func Test_StringInDenyCondition(t *testing.T) {
er := Validate(&PolicyContext{Policy: policy, NewResource: *resourceUnstructured, JSONContext: ctx})
assert.Assert(t, er.IsSuccessful())
}
func Test_foreach_container_pass(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {"name": "test"},
"spec": { "template": { "spec": {
"containers": [
{"name": "pod1-valid", "image": "nginx/nginx:v1"},
{"name": "pod2-valid", "image": "nginx/nginx:v2"},
{"name": "pod3-valid", "image": "nginx/nginx:v3"}
]
}}}}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {"name": "test"},
"spec": {
"rules": [
{
"name": "test-path-not-exist",
"match": {"resources": { "kinds": [ "Deployment" ] } },
"validate": {
"foreach": {
"list": "request.object.spec.template.spec.containers",
"pattern": {
"name": "*-valid"
}
}
}}]}}`)
testForEach(t, policyraw, resourceRaw, "", response.RuleStatusPass)
}
func Test_foreach_container_fail(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {"name": "test"},
"spec": { "template": { "spec": {
"containers": [
{"name": "pod1-valid", "image": "nginx/nginx:v1"},
{"name": "pod2-invalid", "image": "nginx/nginx:v2"},
{"name": "pod3-valid", "image": "nginx/nginx:v3"}
]
}}}}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {"name": "test"},
"spec": {
"rules": [
{
"name": "test",
"match": {"resources": { "kinds": [ "Deployment" ] } },
"validate": {
"foreach": {
"list": "request.object.spec.template.spec.containers",
"pattern": {
"name": "*-valid"
}
}
}}]}}`)
testForEach(t, policyraw, resourceRaw, "", response.RuleStatusFail)
}
func Test_foreach_container_deny_fail(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {"name": "test"},
"spec": { "template": { "spec": {
"containers": [
{"name": "pod1-valid", "image": "nginx/nginx:v1"},
{"name": "pod2-invalid", "image": "docker.io/nginx/nginx:v2"},
{"name": "pod3-valid", "image": "nginx/nginx:v3"}
]
}}}}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {"name": "test"},
"spec": {
"rules": [
{
"name": "test",
"match": {"resources": { "kinds": [ "Deployment" ] } },
"validate": {
"foreach": {
"list": "request.object.spec.template.spec.containers",
"deny": {
"conditions": [
{"key": "{{ regex_match('{{request.object.image}}', 'docker.io') }}", "operator": "Equals", "value": false}
]
}
}
}}]}}`)
testForEach(t, policyraw, resourceRaw, "", response.RuleStatusFail)
}
func Test_foreach_container_deny_success(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {"name": "test"},
"spec": { "template": { "spec": {
"containers": [
{"name": "pod1-valid", "image": "nginx/nginx:v1"},
{"name": "pod2-invalid", "image": "nginx/nginx:v2"},
{"name": "pod3-valid", "image": "nginx/nginx:v3"}
]
}}}}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {"name": "test"},
"spec": {
"rules": [
{
"name": "test",
"match": {"resources": { "kinds": [ "Deployment" ] } },
"validate": {
"foreach": {
"list": "request.object.spec.template.spec.containers",
"deny": {
"conditions": [
{"key": "{{ regex_match('{{request.object.image}}', 'docker.io') }}", "operator": "Equals", "value": false}
]
}
}
}}]}}`)
testForEach(t, policyraw, resourceRaw, "", response.RuleStatusFail)
}
func Test_foreach_container_deny_error(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {"name": "test"},
"spec": { "template": { "spec": {
"containers": [
{"name": "pod1-valid", "image": "nginx/nginx:v1"},
{"name": "pod2-invalid", "image": "nginx/nginx:v2"},
{"name": "pod3-valid", "image": "nginx/nginx:v3"}
]
}}}}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {"name": "test"},
"spec": {
"rules": [
{
"name": "test",
"match": {"resources": { "kinds": [ "Deployment" ] } },
"validate": {
"foreach": {
"list": "request.object.spec.template.spec.containers",
"deny": {
"conditions": [
{"key": "{{ regex_match_INVALID('{{request.object.image}}', 'docker.io') }}", "operator": "Equals", "value": false}
]
}
}
}}]}}`)
testForEach(t, policyraw, resourceRaw, "", response.RuleStatusError)
}
func Test_foreach_context_preconditions(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {"name": "test"},
"spec": { "template": { "spec": {
"containers": [
{"name": "pod1-valid", "image": "nginx/nginx:v1"},
{"name": "pod2-valid", "image": "nginx/nginx:v2"},
{"name": "pod3-valid", "image": "nginx/nginx:v3"},
{"name": "pod4-valid", "image": "nginx/nginx:v4"}
]
}}}}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {"name": "test"},
"spec": {
"rules": [
{
"name": "test",
"match": {"resources": { "kinds": [ "Deployment" ] } },
"validate": {
"foreach": {
"list": "request.object.spec.template.spec.containers",
"context": [{"name": "tags", "configMap": {"name": "mycmap", "namespace": "default"}}],
"preconditions": { "all": [
{
"key": "{{request.object.name}}",
"operator": "Equals",
"value": "pod1-valid | pod2-valid | pod3-valid"
}
]},
"deny": {
"conditions": [
{"key": "images.{{ request.object.name }}.tag", "operator": "NotEquals", "value": "{{ tags.data.{{ request.object.name }} }}"}
]
}
}
}}]}}`)
configMapVariableContext := store.Context{
Policies: []store.Policy{
{
Name: "test",
Rules: []store.Rule{
{
Name: "test",
Values: map[string]string{
"tags.data.pod1-valid": "v1",
"tags.data.pod2-valid": "v2",
"tags.data.pod3-valid": "v3",
},
},
},
},
},
}
store.SetContext(configMapVariableContext)
store.SetMock(true)
testForEach(t, policyraw, resourceRaw, "", response.RuleStatusPass)
}
func Test_foreach_context_preconditions_fail(t *testing.T) {
resourceRaw := []byte(`{
"apiVersion": "v1",
"kind": "Deployment",
"metadata": {"name": "test"},
"spec": { "template": { "spec": {
"containers": [
{"name": "pod1-valid", "image": "nginx/nginx:v1"},
{"name": "pod2-valid", "image": "nginx/nginx:v2"},
{"name": "pod3-valid", "image": "nginx/nginx:v3"},
{"name": "pod4-valid", "image": "nginx/nginx:v4"}
]
}}}}`)
policyraw := []byte(`{
"apiVersion": "kyverno.io/v1",
"kind": "ClusterPolicy",
"metadata": {"name": "test"},
"spec": {
"rules": [
{
"name": "test",
"match": {"resources": { "kinds": [ "Deployment" ] } },
"validate": {
"foreach": {
"list": "request.object.spec.template.spec.containers",
"context": [{"name": "tags", "configMap": {"name": "mycmap", "namespace": "default"}}],
"preconditions": { "all": [
{
"key": "{{request.object.name}}",
"operator": "Equals",
"value": "pod1-valid | pod2-valid | pod3-valid"
}
]},
"deny": {
"conditions": [
{"key": "images.{{ request.object.name }}.tag", "operator": "NotEquals", "value": "{{ tags.data.{{ request.object.name }} }}"}
]
}
}
}}]}}`)
configMapVariableContext := store.Context{
Policies: []store.Policy{
{
Name: "test",
Rules: []store.Rule{
{
Name: "test",
Values: map[string]string{
"tags.data.pod1-valid": "v1",
"tags.data.pod2-valid": "v22",
"tags.data.pod3-valid": "v3",
},
},
},
},
},
}
store.SetContext(configMapVariableContext)
store.SetMock(true)
testForEach(t, policyraw, resourceRaw, "", response.RuleStatusFail)
}
func testForEach(t *testing.T, policyraw []byte, resourceRaw []byte, msg string, status response.RuleStatus) {
var policy kyverno.ClusterPolicy
assert.NilError(t, json.Unmarshal(policyraw, &policy))
resourceUnstructured, err := utils.ConvertToUnstructured(resourceRaw)
assert.NilError(t, err)
ctx := context.NewContext()
err = ctx.AddResource(resourceRaw)
assert.NilError(t, err)
policyContext := &PolicyContext{
Policy: policy,
JSONContext: ctx,
NewResource: *resourceUnstructured}
er := Validate(policyContext)
assert.Equal(t, er.PolicyResponse.Rules[0].Status, status)
if msg != "" {
assert.Equal(t, er.PolicyResponse.Rules[0].Message, msg)
}
}

View file

@ -108,7 +108,7 @@ func UntypedToRule(untyped interface{}) (kyverno.Rule, error) {
func substituteAll(log logr.Logger, ctx context.EvalInterface, document interface{}, resolver VariableResolver) (_ interface{}, err error) {
document, err = substituteReferences(log, document)
if err != nil {
return kyverno.Rule{}, err
return document, err
}
return substituteVars(log, ctx, document, resolver)