Implement validationActions and auditAnnotations

Kubernetes-commit: d221ddb89a5dde5a6f55674dc38aa71cc842d481
This commit is contained in:
Joe Betz 2023-03-06 17:29:28 -05:00 committed by Kubernetes Publisher
parent 1e9e30ec0b
commit 265820879d
13 changed files with 1059 additions and 158 deletions

View File

@ -109,3 +109,15 @@ func (m *ValidatingAdmissionPolicyMetrics) ObserveRejection(ctx context.Context,
m.policyCheck.WithContext(ctx).WithLabelValues(policy, binding, "deny", state).Inc()
m.policyLatency.WithContext(ctx).WithLabelValues(policy, binding, "deny", state).Observe(elapsed.Seconds())
}
// ObserveAudit observes a policy validation audit annotation was published for a validation failure.
func (m *ValidatingAdmissionPolicyMetrics) ObserveAudit(ctx context.Context, elapsed time.Duration, policy, binding, state string) {
m.policyCheck.WithContext(ctx).WithLabelValues(policy, binding, "audit", state).Inc()
m.policyLatency.WithContext(ctx).WithLabelValues(policy, binding, "audit", state).Observe(elapsed.Seconds())
}
// ObserveWarn observes a policy validation warning was published for a validation failure.
func (m *ValidatingAdmissionPolicyMetrics) ObserveWarn(ctx context.Context, elapsed time.Duration, policy, binding, state string) {
m.policyCheck.WithContext(ctx).WithLabelValues(policy, binding, "warn", state).Inc()
m.policyLatency.WithContext(ctx).WithLabelValues(policy, binding, "warn", state).Observe(elapsed.Seconds())
}

View File

@ -223,11 +223,26 @@ func CompileCELExpression(expressionAccessor ExpressionAccessor, optionalVars Op
ExpressionAccessor: expressionAccessor,
}
}
if ast.OutputType() != cel.BoolType {
found := false
returnTypes := expressionAccessor.ReturnTypes()
for _, returnType := range returnTypes {
if ast.OutputType() == returnType {
found = true
break
}
}
if !found {
var reason string
if len(returnTypes) == 1 {
reason = fmt.Sprintf("must evaluate to %v", returnTypes[0].String())
} else {
reason = fmt.Sprintf("must evaluate to one of %v", returnTypes)
}
return CompilationResult{
Error: &apiservercel.Error{
Type: apiservercel.ErrorTypeInvalid,
Detail: "cel expression must evaluate to a bool",
Detail: reason,
},
ExpressionAccessor: expressionAccessor,
}

View File

@ -20,6 +20,8 @@ import (
"strings"
"testing"
celgo "github.com/google/cel-go/cel"
celconfig "k8s.io/apiserver/pkg/apis/cel"
)
@ -120,34 +122,79 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
for _, expr := range tc.expressions {
result := CompileCELExpression(&fakeExpressionAccessor{
expr,
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: true}, celconfig.PerCallLimit)
if result.Error != nil {
t.Errorf("Unexpected error: %v", result.Error)
}
t.Run(expr, func(t *testing.T) {
t.Run("expression", func(t *testing.T) {
result := CompileCELExpression(&fakeValidationCondition{
Expression: expr,
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit)
if result.Error != nil {
t.Errorf("Unexpected error: %v", result.Error)
}
})
t.Run("auditAnnotation.valueExpression", func(t *testing.T) {
// Test audit annotation compilation by casting the result to a string
result := CompileCELExpression(&fakeAuditAnnotationCondition{
ValueExpression: "string(" + expr + ")",
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit)
if result.Error != nil {
t.Errorf("Unexpected error: %v", result.Error)
}
})
})
}
for expr, expectErr := range tc.errorExpressions {
result := CompileCELExpression(&fakeExpressionAccessor{
expr,
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit)
if result.Error == nil {
t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
continue
}
if !strings.Contains(result.Error.Error(), expectErr) {
t.Errorf("Expected compilation '%s' error to contain '%v' but got: %v", expr, expectErr, result.Error)
}
continue
t.Run(expr, func(t *testing.T) {
t.Run("expression", func(t *testing.T) {
result := CompileCELExpression(&fakeValidationCondition{
Expression: expr,
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit)
if result.Error == nil {
t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
return
}
if !strings.Contains(result.Error.Error(), expectErr) {
t.Errorf("Expected validation '%s' error to contain '%v' but got: %v", expr, expectErr, result.Error)
}
})
t.Run("auditAnnotation.valueExpression", func(t *testing.T) {
// Test audit annotation compilation by casting the result to a string
result := CompileCELExpression(&fakeAuditAnnotationCondition{
ValueExpression: "string(" + expr + ")",
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit)
if result.Error == nil {
t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
return
}
if !strings.Contains(result.Error.Error(), expectErr) {
t.Errorf("Expected validation '%s' error to contain '%v' but got: %v", expr, expectErr, result.Error)
}
})
})
}
})
}
}
type fakeExpressionAccessor struct {
expression string
type fakeValidationCondition struct {
Expression string
}
func (f *fakeExpressionAccessor) GetExpression() string {
return f.expression
func (v *fakeValidationCondition) GetExpression() string {
return v.Expression
}
func (v *fakeValidationCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.BoolType}
}
type fakeAuditAnnotationCondition struct {
ValueExpression string
}
func (v *fakeAuditAnnotationCondition) GetExpression() string {
return v.ValueExpression
}
func (v *fakeAuditAnnotationCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.StringType, celgo.NullType}
}

View File

@ -75,11 +75,7 @@ func (a *evaluationActivation) Parent() interpreter.Activation {
}
// Compile compiles the cel expressions defined in the ExpressionAccessors into a Filter
// perCallLimit was added for testing purpose only. Callers should always use const PerCallLimit from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, perCallLimit uint64) Filter {
if len(expressionAccessors) == 0 {
return nil
}
compilationResults := make([]CompilationResult, len(expressionAccessors))
for i, expressionAccessor := range expressionAccessors {
compilationResults[i] = CompileCELExpression(expressionAccessor, options, perCallLimit)

View File

@ -24,14 +24,10 @@ import (
"strings"
"testing"
celgo "github.com/google/cel-go/cel"
celtypes "github.com/google/cel-go/common/types"
"github.com/stretchr/testify/require"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
apiservercel "k8s.io/apiserver/pkg/cel"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
@ -39,6 +35,10 @@ import (
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
apiservercel "k8s.io/apiserver/pkg/cel"
)
type condition struct {
@ -49,6 +49,10 @@ func (c *condition) GetExpression() string {
return c.Expression
}
func (v *condition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.BoolType}
}
func TestCompile(t *testing.T) {
cases := []struct {
name string

View File

@ -19,6 +19,7 @@ package cel
import (
"time"
"github.com/google/cel-go/cel"
"github.com/google/cel-go/common/types/ref"
v1 "k8s.io/api/admission/v1"
@ -31,6 +32,7 @@ var _ ExpressionAccessor = &MatchCondition{}
type ExpressionAccessor interface {
GetExpression() string
ReturnTypes() []*cel.Type
}
// EvaluationResult contains the minimal required fields and metadata of a cel evaluation
@ -50,6 +52,10 @@ func (v *MatchCondition) GetExpression() string {
return v.Expression
}
func (v *MatchCondition) ReturnTypes() []*cel.Type {
return []*cel.Type{cel.BoolType}
}
// OptionalVariableDeclarations declares which optional CEL variables
// are declared for an expression.
type OptionalVariableDeclarations struct {
@ -83,6 +89,7 @@ type OptionalVariableBindings struct {
// Filter contains a function to evaluate compiled CEL-typed values
// It expects the inbound object to already have been converted to the version expected
// by the underlying CEL code (which is indicated by the match criteria of a policy definition).
// versionedParams may be nil.
type Filter interface {
// ForInput converts compiled CEL-typed values into evaluated CEL-typed values
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.

View File

@ -19,11 +19,13 @@ package validatingadmissionpolicy
import (
"context"
"fmt"
"strings"
"sync"
"sync/atomic"
"testing"
"time"
celgo "github.com/google/cel-go/cel"
"github.com/stretchr/testify/require"
"k8s.io/klog/v2"
@ -38,14 +40,18 @@ import (
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
utiljson "k8s.io/apimachinery/pkg/util/json"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/admission/initializer"
"k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
auditinternal "k8s.io/apiserver/pkg/apis/audit"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/features"
"k8s.io/apiserver/pkg/warning"
dynamicfake "k8s.io/client-go/dynamic/fake"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes/fake"
@ -144,6 +150,7 @@ var (
Name: fakeParams.GetName(),
Namespace: fakeParams.GetNamespace(),
},
ValidationActions: []v1alpha1.ValidationAction{v1alpha1.Deny},
},
}
denyBindingWithNoParamRef *v1alpha1.ValidatingAdmissionPolicyBinding = &v1alpha1.ValidatingAdmissionPolicyBinding{
@ -152,7 +159,39 @@ var (
ResourceVersion: "1",
},
Spec: v1alpha1.ValidatingAdmissionPolicyBindingSpec{
PolicyName: denyPolicy.Name,
PolicyName: denyPolicy.Name,
ValidationActions: []v1alpha1.ValidationAction{v1alpha1.Deny},
},
}
denyBindingWithAudit = &v1alpha1.ValidatingAdmissionPolicyBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "denybinding.example.com",
ResourceVersion: "1",
},
Spec: v1alpha1.ValidatingAdmissionPolicyBindingSpec{
PolicyName: denyPolicy.Name,
ValidationActions: []v1alpha1.ValidationAction{v1alpha1.Audit},
},
}
denyBindingWithWarn = &v1alpha1.ValidatingAdmissionPolicyBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "denybinding.example.com",
ResourceVersion: "1",
},
Spec: v1alpha1.ValidatingAdmissionPolicyBindingSpec{
PolicyName: denyPolicy.Name,
ValidationActions: []v1alpha1.ValidationAction{v1alpha1.Warn},
},
}
denyBindingWithAll = &v1alpha1.ValidatingAdmissionPolicyBinding{
ObjectMeta: metav1.ObjectMeta{
Name: "denybinding.example.com",
ResourceVersion: "1",
},
Spec: v1alpha1.ValidatingAdmissionPolicyBindingSpec{
PolicyName: denyPolicy.Name,
ValidationActions: []v1alpha1.ValidationAction{v1alpha1.Deny, v1alpha1.Warn, v1alpha1.Audit},
},
}
)
@ -174,12 +213,13 @@ func (f *fakeCompiler) Compile(
options cel.OptionalVariableDeclarations,
perCallLimit uint64,
) cel.Filter {
key := expressions[0].GetExpression()
if fun, ok := f.CompileFuncs[key]; ok {
return fun(expressions, options)
if len(expressions) > 0 {
key := expressions[0].GetExpression()
if fun, ok := f.CompileFuncs[key]; ok {
return fun(expressions, options)
}
}
return nil
return &fakeFilter{}
}
func (f *fakeCompiler) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, compileFunc func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter) {
@ -203,6 +243,10 @@ func (f *fakeEvalRequest) GetExpression() string {
return ""
}
func (f *fakeEvalRequest) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.BoolType}
}
var _ cel.Filter = &fakeFilter{}
type fakeFilter struct {
@ -220,22 +264,28 @@ func (f *fakeFilter) CompilationErrors() []error {
var _ Validator = &fakeValidator{}
type fakeValidator struct {
*fakeFilter
ValidateFunc func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision
validationFilter, auditAnnotationFilter *fakeFilter
ValidateFunc func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult
}
func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, validateFunc func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision) {
func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, validateFunc func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult) {
//Key must be something that we can decipher from the inputs to Validate so using message which will be on the validationCondition object of evalResult
validateKey := definition.Spec.Validations[0].Expression
var key string
if len(definition.Spec.Validations) > 0 {
key = definition.Spec.Validations[0].Expression
} else {
key = definition.Spec.AuditAnnotations[0].Key
}
if validatorMap == nil {
validatorMap = make(map[string]*fakeValidator)
}
f.ValidateFunc = validateFunc
validatorMap[validateKey] = f
validatorMap[key] = f
}
func (f *fakeValidator) Validate(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
func (f *fakeValidator) Validate(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
return f.ValidateFunc(versionedAttr, versionedParams, runtimeCELCostBudget)
}
@ -369,10 +419,11 @@ func setupTestCommon(t *testing.T, compiler cel.FilterCompiler, matcher Matcher,
// Override compiler used by controller for tests
controller = handler.evaluator.(*celAdmissionController)
controller.policyController.filterCompiler = compiler
controller.policyController.newValidator = func(filter cel.Filter, fail *admissionRegistrationv1.FailurePolicyType, authorizer authorizer.Authorizer) Validator {
f := filter.(*fakeFilter)
controller.policyController.newValidator = func(validationFilter, auditAnnotationFilter cel.Filter, fail *admissionRegistrationv1.FailurePolicyType, authorizer authorizer.Authorizer) Validator {
f := validationFilter.(*fakeFilter)
v := validatorMap[f.keyId]
v.fakeFilter = f
v.validationFilter = f
v.auditAnnotationFilter = auditAnnotationFilter.(*fakeFilter)
return v
}
controller.policyController.matcher = matcher
@ -596,7 +647,7 @@ func waitForReconcileDeletion(ctx context.Context, controller *celAdmissionContr
func attributeRecord(
old, new runtime.Object,
operation admission.Operation,
) admission.Attributes {
) *FakeAttributes {
if old == nil && new == nil {
panic("both `old` and `new` may not be nil")
}
@ -622,19 +673,21 @@ func attributeRecord(
panic(err)
}
return admission.NewAttributesRecord(
new,
old,
gvk,
accessor.GetNamespace(),
accessor.GetName(),
mapping.Resource,
"",
operation,
nil,
false,
nil,
)
return &FakeAttributes{
Attributes: admission.NewAttributesRecord(
new,
old,
gvk,
accessor.GetNamespace(),
accessor.GetName(),
mapping.Resource,
"",
operation,
nil,
false,
nil,
),
}
}
func ptrTo[T any](obj T) *T {
@ -716,11 +769,13 @@ func TestBasicPolicyDefinitionFailure(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
return []PolicyDecision{
{
Action: ActionDeny,
Message: "Denied",
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: ActionDeny,
Message: "Denied",
},
},
}
})
@ -737,14 +792,22 @@ func TestBasicPolicyDefinitionFailure(t *testing.T) {
testContext, controller,
fakeParams, denyBinding, denyPolicy))
warningRecorder := newWarningRecorder()
warnCtx := warning.WithWarningRecorder(testContext, warningRecorder)
attr := attributeRecord(nil, fakeParams, admission.Create)
err := handler.Validate(
testContext,
warnCtx,
// Object is irrelevant/unchecked for this test. Just test that
// the evaluator is executed, and returns a denial
attributeRecord(nil, fakeParams, admission.Create),
attr,
&admission.RuntimeObjectInterfaces{},
)
require.Equal(t, 0, warningRecorder.len())
annotations := attr.GetAnnotations(auditinternal.LevelMetadata)
require.Equal(t, 0, len(annotations))
require.ErrorContains(t, err, `Denied`)
}
@ -776,11 +839,13 @@ func TestDefinitionDoesntMatch(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
return []PolicyDecision{
{
Action: ActionDeny,
Message: "Denied",
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: ActionDeny,
Message: "Denied",
},
},
}
})
@ -887,11 +952,13 @@ func TestReconfigureBinding(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
return []PolicyDecision{
{
Action: ActionDeny,
Message: "Denied",
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: ActionDeny,
Message: "Denied",
},
},
}
})
@ -907,6 +974,7 @@ func TestReconfigureBinding(t *testing.T) {
Name: fakeParams2.GetName(),
Namespace: fakeParams2.GetNamespace(),
},
ValidationActions: []v1alpha1.ValidationAction{v1alpha1.Deny},
},
}
@ -994,11 +1062,13 @@ func TestRemoveDefinition(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
return []PolicyDecision{
{
Action: ActionDeny,
Message: "Denied",
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: ActionDeny,
Message: "Denied",
},
},
}
})
@ -1061,11 +1131,13 @@ func TestRemoveBinding(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
return []PolicyDecision{
{
Action: ActionDeny,
Message: "Denied",
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: ActionDeny,
Message: "Denied",
},
},
}
})
@ -1169,11 +1241,13 @@ func TestInvalidParamSourceInstanceName(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
return []PolicyDecision{
{
Action: ActionDeny,
Message: "Denied",
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: ActionDeny,
Message: "Denied",
},
},
}
})
@ -1235,11 +1309,13 @@ func TestEmptyParamSource(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
return []PolicyDecision{
{
Action: ActionDeny,
Message: "Denied",
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: ActionDeny,
Message: "Denied",
},
},
}
})
@ -1335,11 +1411,13 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) {
}
})
validator1.RegisterDefinition(&policy1, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
validator1.RegisterDefinition(&policy1, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
evaluations1.Add(1)
return []PolicyDecision{
{
Action: ActionAdmit,
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: ActionAdmit,
},
},
}
})
@ -1352,12 +1430,14 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) {
}
})
validator2.RegisterDefinition(&policy2, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
validator2.RegisterDefinition(&policy2, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
evaluations2.Add(1)
return []PolicyDecision{
{
Action: ActionDeny,
Message: "Policy2Denied",
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: ActionDeny,
Message: "Policy2Denied",
},
},
}
})
@ -1460,20 +1540,24 @@ func TestNativeTypeParam(t *testing.T) {
}
})
validator.RegisterDefinition(&nativeTypeParamPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
validator.RegisterDefinition(&nativeTypeParamPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
evaluations.Add(1)
if _, ok := versionedParams.(*v1.ConfigMap); ok {
return []PolicyDecision{
{
Action: ActionDeny,
Message: "correct type",
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: ActionDeny,
Message: "correct type",
},
},
}
}
return []PolicyDecision{
{
Action: ActionDeny,
Message: "Incorrect param type",
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: ActionDeny,
Message: "Incorrect param type",
},
},
}
})
@ -1516,6 +1600,366 @@ func TestNativeTypeParam(t *testing.T) {
require.EqualValues(t, 1, evaluations.Load())
}
func TestAuditValidationAction(t *testing.T) {
testContext, testContextCancel := context.WithCancel(context.Background())
defer testContextCancel()
compiler := &fakeCompiler{}
validator := &fakeValidator{}
matcher := &fakeMatcher{
DefaultMatch: true,
}
handler, _, tracker, controller := setupFakeTest(t, compiler, matcher)
// Push some fake
noParamSourcePolicy := *denyPolicy
noParamSourcePolicy.Spec.ParamKind = nil
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
return &fakeFilter{
keyId: denyPolicy.Spec.Validations[0].Expression,
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: ActionDeny,
Message: "I'm sorry Dave",
},
},
}
})
require.NoError(t, tracker.Create(definitionsGVR, &noParamSourcePolicy, noParamSourcePolicy.Namespace))
require.NoError(t, tracker.Create(bindingsGVR, denyBindingWithAudit, denyBindingWithAudit.Namespace))
// Wait for controller to reconcile given objects
require.NoError(t,
waitForReconcile(
testContext, controller,
denyBindingWithAudit, &noParamSourcePolicy))
attr := attributeRecord(nil, fakeParams, admission.Create)
warningRecorder := newWarningRecorder()
warnCtx := warning.WithWarningRecorder(testContext, warningRecorder)
err := handler.Validate(
warnCtx,
attr,
&admission.RuntimeObjectInterfaces{},
)
require.Equal(t, 0, warningRecorder.len())
annotations := attr.GetAnnotations(auditinternal.LevelMetadata)
require.Equal(t, 1, len(annotations))
valueJson, ok := annotations["validation.policy.admission.k8s.io/validation_failure"]
require.True(t, ok)
var value []validationFailureValue
jsonErr := utiljson.Unmarshal([]byte(valueJson), &value)
require.NoError(t, jsonErr)
expected := []validationFailureValue{{
ExpressionIndex: 0,
Message: "I'm sorry Dave",
ValidationActions: []v1alpha1.ValidationAction{v1alpha1.Audit},
Binding: "denybinding.example.com",
Policy: noParamSourcePolicy.Name,
}}
require.Equal(t, expected, value)
require.NoError(t, err)
}
func TestWarnValidationAction(t *testing.T) {
testContext, testContextCancel := context.WithCancel(context.Background())
defer testContextCancel()
compiler := &fakeCompiler{}
validator := &fakeValidator{}
matcher := &fakeMatcher{
DefaultMatch: true,
}
handler, _, tracker, controller := setupFakeTest(t, compiler, matcher)
// Push some fake
noParamSourcePolicy := *denyPolicy
noParamSourcePolicy.Spec.ParamKind = nil
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
return &fakeFilter{
keyId: denyPolicy.Spec.Validations[0].Expression,
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: ActionDeny,
Message: "I'm sorry Dave",
},
},
}
})
require.NoError(t, tracker.Create(definitionsGVR, &noParamSourcePolicy, noParamSourcePolicy.Namespace))
require.NoError(t, tracker.Create(bindingsGVR, denyBindingWithWarn, denyBindingWithWarn.Namespace))
// Wait for controller to reconcile given objects
require.NoError(t,
waitForReconcile(
testContext, controller,
denyBindingWithWarn, &noParamSourcePolicy))
attr := attributeRecord(nil, fakeParams, admission.Create)
warningRecorder := newWarningRecorder()
warnCtx := warning.WithWarningRecorder(testContext, warningRecorder)
err := handler.Validate(
warnCtx,
attr,
&admission.RuntimeObjectInterfaces{},
)
require.Equal(t, 1, warningRecorder.len())
require.True(t, warningRecorder.hasWarning("Validation failed for ValidatingAdmissionPolicy 'denypolicy.example.com' with binding 'denybinding.example.com': I'm sorry Dave"))
annotations := attr.GetAnnotations(auditinternal.LevelMetadata)
require.Equal(t, 0, len(annotations))
require.NoError(t, err)
}
func TestAllValidationActions(t *testing.T) {
testContext, testContextCancel := context.WithCancel(context.Background())
defer testContextCancel()
compiler := &fakeCompiler{}
validator := &fakeValidator{}
matcher := &fakeMatcher{
DefaultMatch: true,
}
handler, _, tracker, controller := setupFakeTest(t, compiler, matcher)
// Push some fake
noParamSourcePolicy := *denyPolicy
noParamSourcePolicy.Spec.ParamKind = nil
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
return &fakeFilter{
keyId: denyPolicy.Spec.Validations[0].Expression,
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: ActionDeny,
Message: "I'm sorry Dave",
},
},
}
})
require.NoError(t, tracker.Create(definitionsGVR, &noParamSourcePolicy, noParamSourcePolicy.Namespace))
require.NoError(t, tracker.Create(bindingsGVR, denyBindingWithAll, denyBindingWithAll.Namespace))
// Wait for controller to reconcile given objects
require.NoError(t,
waitForReconcile(
testContext, controller,
denyBindingWithAll, &noParamSourcePolicy))
attr := attributeRecord(nil, fakeParams, admission.Create)
warningRecorder := newWarningRecorder()
warnCtx := warning.WithWarningRecorder(testContext, warningRecorder)
err := handler.Validate(
warnCtx,
attr,
&admission.RuntimeObjectInterfaces{},
)
require.Equal(t, 1, warningRecorder.len())
require.True(t, warningRecorder.hasWarning("Validation failed for ValidatingAdmissionPolicy 'denypolicy.example.com' with binding 'denybinding.example.com': I'm sorry Dave"))
annotations := attr.GetAnnotations(auditinternal.LevelMetadata)
require.Equal(t, 1, len(annotations))
valueJson, ok := annotations["validation.policy.admission.k8s.io/validation_failure"]
require.True(t, ok)
var value []validationFailureValue
jsonErr := utiljson.Unmarshal([]byte(valueJson), &value)
require.NoError(t, jsonErr)
expected := []validationFailureValue{{
ExpressionIndex: 0,
Message: "I'm sorry Dave",
ValidationActions: []v1alpha1.ValidationAction{v1alpha1.Deny, v1alpha1.Warn, v1alpha1.Audit},
Binding: "denybinding.example.com",
Policy: noParamSourcePolicy.Name,
}}
require.Equal(t, expected, value)
require.ErrorContains(t, err, "I'm sorry Dave")
}
func TestAuditAnnotations(t *testing.T) {
testContext, testContextCancel := context.WithCancel(context.Background())
defer testContextCancel()
compiler := &fakeCompiler{}
validator := &fakeValidator{}
matcher := &fakeMatcher{
DefaultMatch: true,
}
handler, paramsTracker, tracker, controller := setupFakeTest(t, compiler, matcher)
// Push some fake
policy := *denyPolicy
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
return &fakeFilter{
keyId: denyPolicy.Spec.Validations[0].Expression,
}
})
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
o, err := meta.Accessor(versionedParams)
if err != nil {
t.Fatal(err)
}
exampleValue := "normal-value"
if o.GetName() == "replicas-test2.example.com" {
exampleValue = "special-value"
}
return ValidateResult{
AuditAnnotations: []PolicyAuditAnnotation{
{
Key: "example-key",
Value: exampleValue,
Action: AuditAnnotationActionPublish,
},
{
Key: "excluded-key",
Value: "excluded-value",
Action: AuditAnnotationActionExclude,
},
{
Key: "error-key",
Action: AuditAnnotationActionError,
Error: "example error",
},
},
}
})
fakeParams2 := fakeParams.DeepCopy()
fakeParams2.SetName("replicas-test2.example.com")
denyBinding2 := denyBinding.DeepCopy()
denyBinding2.SetName("denybinding2.example.com")
denyBinding2.Spec.ParamRef.Name = fakeParams2.GetName()
fakeParams3 := fakeParams.DeepCopy()
fakeParams3.SetName("replicas-test3.example.com")
denyBinding3 := denyBinding.DeepCopy()
denyBinding3.SetName("denybinding3.example.com")
denyBinding3.Spec.ParamRef.Name = fakeParams3.GetName()
require.NoError(t, paramsTracker.Add(fakeParams))
require.NoError(t, paramsTracker.Add(fakeParams2))
require.NoError(t, paramsTracker.Add(fakeParams3))
require.NoError(t, tracker.Create(definitionsGVR, &policy, policy.Namespace))
require.NoError(t, tracker.Create(bindingsGVR, denyBinding, denyBinding.Namespace))
require.NoError(t, tracker.Create(bindingsGVR, denyBinding2, denyBinding2.Namespace))
require.NoError(t, tracker.Create(bindingsGVR, denyBinding3, denyBinding3.Namespace))
// Wait for controller to reconcile given objects
require.NoError(t,
waitForReconcile(
testContext, controller,
denyBinding, denyBinding2, denyBinding3, denyPolicy, fakeParams, fakeParams2, fakeParams3))
attr := attributeRecord(nil, fakeParams, admission.Create)
err := handler.Validate(
testContext,
attr,
&admission.RuntimeObjectInterfaces{},
)
annotations := attr.GetAnnotations(auditinternal.LevelMetadata)
require.Equal(t, 1, len(annotations))
value := annotations[policy.Name+"/example-key"]
parts := strings.Split(value, ", ")
require.Equal(t, 2, len(parts))
require.Contains(t, parts, "normal-value", "special-value")
require.ErrorContains(t, err, "example error")
}
// FakeAttributes decorates admission.Attributes. It's used to trace the added annotations.
type FakeAttributes struct {
admission.Attributes
annotations map[string]string
mutex sync.Mutex
}
// AddAnnotation adds an annotation key value pair to FakeAttributes
func (f *FakeAttributes) AddAnnotation(k, v string) error {
return f.AddAnnotationWithLevel(k, v, auditinternal.LevelMetadata)
}
// AddAnnotationWithLevel adds an annotation key value pair to FakeAttributes
func (f *FakeAttributes) AddAnnotationWithLevel(k, v string, _ auditinternal.Level) error {
f.mutex.Lock()
defer f.mutex.Unlock()
if err := f.Attributes.AddAnnotation(k, v); err != nil {
return err
}
if f.annotations == nil {
f.annotations = make(map[string]string)
}
f.annotations[k] = v
return nil
}
// GetAnnotations reads annotations from FakeAttributes
func (f *FakeAttributes) GetAnnotations(_ auditinternal.Level) map[string]string {
f.mutex.Lock()
defer f.mutex.Unlock()
annotations := make(map[string]string, len(f.annotations))
for k, v := range f.annotations {
annotations[k] = v
}
return annotations
}
type warningRecorder struct {
sync.Mutex
warnings sets.Set[string]
}
func newWarningRecorder() *warningRecorder {
return &warningRecorder{warnings: sets.New[string]()}
}
func (r *warningRecorder) AddWarning(_, text string) {
r.Lock()
defer r.Unlock()
r.warnings.Insert(text)
return
}
func (r *warningRecorder) hasWarning(text string) bool {
r.Lock()
defer r.Unlock()
return r.warnings.Has(text)
}
func (r *warningRecorder) len() int {
r.Lock()
defer r.Unlock()
return len(r.warnings)
}
type fakeAuthorizer struct{}
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {

View File

@ -20,16 +20,19 @@ import (
"context"
"errors"
"fmt"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"strings"
"sync"
"sync/atomic"
"time"
"k8s.io/klog/v2"
"k8s.io/api/admissionregistration/v1alpha1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
utiljson "k8s.io/apimachinery/pkg/util/json"
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/wait"
@ -39,7 +42,9 @@ import (
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching"
whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
celconfig "k8s.io/apiserver/pkg/apis/cel"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/warning"
"k8s.io/client-go/dynamic"
"k8s.io/client-go/informers"
"k8s.io/client-go/kubernetes"
@ -169,6 +174,8 @@ func (c *celAdmissionController) Run(stopCh <-chan struct{}) {
wg.Wait()
}
const maxAuditAnnotationValueLength = 10 * 1024
func (c *celAdmissionController) Validate(
ctx context.Context,
a admission.Attributes,
@ -239,6 +246,7 @@ func (c *celAdmissionController) Validate(
continue
}
auditAnnotationCollector := newAuditAnnotationCollector()
for _, bindingInfo := range definitionInfo.bindings {
// If the key is inside dependentBindings, there is guaranteed to
// be a bindingInfo for it
@ -321,27 +329,72 @@ func (c *celAdmissionController) Validate(
versionedAttr = va
}
decisions := bindingInfo.validator.Validate(versionedAttr, param, celconfig.RuntimeCELCostBudget)
validationResult := bindingInfo.validator.Validate(versionedAttr, param, celconfig.RuntimeCELCostBudget)
if err != nil {
// runtime error. Apply failure policy
wrappedError := fmt.Errorf("failed to evaluate CEL expression: %w", err)
addConfigError(wrappedError, definition, binding)
continue
}
for _, decision := range decisions {
for i, decision := range validationResult.Decisions {
switch decision.Action {
case ActionAdmit:
if decision.Evaluation == EvalError {
celmetrics.Metrics.ObserveAdmissionWithError(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
}
case ActionDeny:
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
Definition: definition,
Binding: binding,
PolicyDecision: decision,
})
celmetrics.Metrics.ObserveRejection(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
for _, action := range binding.Spec.ValidationActions {
switch action {
case v1alpha1.Deny:
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
Definition: definition,
Binding: binding,
PolicyDecision: decision,
})
celmetrics.Metrics.ObserveRejection(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
case v1alpha1.Audit:
c.publishValidationFailureAnnotation(binding, i, decision, versionedAttr)
celmetrics.Metrics.ObserveAudit(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
case v1alpha1.Warn:
warning.AddWarning(ctx, "", fmt.Sprintf("Validation failed for ValidatingAdmissionPolicy '%s' with binding '%s': %s", definition.Name, binding.Name, decision.Message))
celmetrics.Metrics.ObserveWarn(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
}
}
default:
return fmt.Errorf("unrecognized evaluation decision '%s' for ValidatingAdmissionPolicyBinding '%s' with ValidatingAdmissionPolicy '%s'",
decision.Action, binding.Name, definition.Name)
}
}
for _, auditAnnotation := range validationResult.AuditAnnotations {
switch auditAnnotation.Action {
case AuditAnnotationActionPublish:
value := auditAnnotation.Value
if len(auditAnnotation.Value) > maxAuditAnnotationValueLength {
value = value[:maxAuditAnnotationValueLength]
}
auditAnnotationCollector.add(auditAnnotation.Key, value)
case AuditAnnotationActionError:
// When failurePolicy=fail, audit annotation errors result in deny
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
Definition: definition,
Binding: binding,
PolicyDecision: PolicyDecision{
Action: ActionDeny,
Evaluation: EvalError,
Message: auditAnnotation.Error,
Elapsed: auditAnnotation.Elapsed,
},
})
celmetrics.Metrics.ObserveRejection(ctx, auditAnnotation.Elapsed, definition.Name, binding.Name, "active")
case AuditAnnotationActionExclude: // skip it
default:
return fmt.Errorf("unsupported AuditAnnotation Action: %s", auditAnnotation.Action)
}
}
}
auditAnnotationCollector.publish(definition.Name, a)
}
if len(deniedDecisions) > 0 {
@ -366,6 +419,25 @@ func (c *celAdmissionController) Validate(
return nil
}
func (c *celAdmissionController) publishValidationFailureAnnotation(binding *v1alpha1.ValidatingAdmissionPolicyBinding, expressionIndex int, decision PolicyDecision, attributes admission.Attributes) {
key := "validation.policy.admission.k8s.io/validation_failure"
// Marshal to a list of failures since, in the future, we may need to support multiple failures
valueJson, err := utiljson.Marshal([]validationFailureValue{{
ExpressionIndex: expressionIndex,
Message: decision.Message,
ValidationActions: binding.Spec.ValidationActions,
Binding: binding.Name,
Policy: binding.Spec.PolicyName,
}})
if err != nil {
klog.Warningf("Failed to set admission audit annotation %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, binding.Spec.PolicyName, binding.Name, err)
}
value := string(valueJson)
if err := attributes.AddAnnotation(key, value); err != nil {
klog.Warningf("Failed to set admission audit annotation %s to %s for ValidatingAdmissionPolicy %s and ValidatingAdmissionPolicyBinding %s: %v", key, value, binding.Spec.PolicyName, binding.Name, err)
}
}
func (c *celAdmissionController) HasSynced() bool {
return c.policyController.HasSynced() && c.definitions.Load() != nil
}
@ -377,3 +449,48 @@ func (c *celAdmissionController) ValidateInitialization() error {
func (c *celAdmissionController) refreshPolicies() {
c.definitions.Store(c.policyController.latestPolicyData())
}
// validationFailureValue defines the JSON format of a "validation.policy.admission.k8s.io/validation_failure" audit
// annotation value.
type validationFailureValue struct {
Message string `json:"message"`
Policy string `json:"policy"`
Binding string `json:"binding"`
ExpressionIndex int `json:"expressionIndex"`
ValidationActions []v1alpha1.ValidationAction `json:"validationActions"`
}
type auditAnnotationCollector struct {
annotations map[string][]string
}
func newAuditAnnotationCollector() auditAnnotationCollector {
return auditAnnotationCollector{annotations: map[string][]string{}}
}
func (a auditAnnotationCollector) add(key, value string) {
// If multiple bindings produces the exact same key and value for an audit annotation,
// ignore the duplicates.
for _, v := range a.annotations[key] {
if v == value {
return
}
}
a.annotations[key] = append(a.annotations[key], value)
}
func (a auditAnnotationCollector) publish(policyName string, attributes admission.Attributes) {
for key, bindingAnnotations := range a.annotations {
var value string
if len(bindingAnnotations) == 1 {
value = bindingAnnotations[0]
} else {
// Multiple distinct values can exist when binding params are used in the valueExpression of an auditAnnotation.
// When this happens, the values are concatenated into a comma-separated list.
value = strings.Join(bindingAnnotations, ", ")
}
if err := attributes.AddAnnotation(policyName+"/"+key, value); err != nil {
klog.Warningf("Failed to set admission audit annotation %s to %s for ValidatingAdmissionPolicy %s: %v", key, value, policyName, err)
}
}
}

View File

@ -92,7 +92,7 @@ type policyController struct {
authz authorizer.Authorizer
}
type newValidator func(cel.Filter, *v1.FailurePolicyType, authorizer.Authorizer) Validator
type newValidator func(validationFilter cel.Filter, auditAnnotationFilter cel.Filter, failurePolicy *v1.FailurePolicyType, authorizer authorizer.Authorizer) Validator
func newPolicyController(
restMapper meta.RESTMapper,
@ -461,6 +461,7 @@ func (c *policyController) latestPolicyData() []policyData {
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
bindingInfo.validator = c.newValidator(
c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars, celconfig.PerCallLimit),
c.filterCompiler.Compile(convertv1alpha1AuditAnnotations(definitionInfo.lastReconciledValue.Spec.AuditAnnotations), optionalVars, celconfig.PerCallLimit),
convertv1alpha1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy),
c.authz,
)
@ -513,6 +514,18 @@ func convertv1alpha1Validations(inputValidations []v1alpha1.Validation) []cel.Ex
return celExpressionAccessor
}
func convertv1alpha1AuditAnnotations(inputValidations []v1alpha1.AuditAnnotation) []cel.ExpressionAccessor {
celExpressionAccessor := make([]cel.ExpressionAccessor, len(inputValidations))
for i, validation := range inputValidations {
validation := AuditAnnotationCondition{
Key: validation.Key,
ValueExpression: validation.ValueExpression,
}
celExpressionAccessor[i] = &validation
}
return celExpressionAccessor
}
func getNamespaceName(namespace, name string) namespacedName {
return namespacedName{
namespace: namespace,

View File

@ -17,6 +17,8 @@ limitations under the License.
package validatingadmissionpolicy
import (
celgo "github.com/google/cel-go/cel"
"k8s.io/api/admissionregistration/v1alpha1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
@ -39,6 +41,24 @@ func (v *ValidationCondition) GetExpression() string {
return v.Expression
}
func (v *ValidationCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.BoolType}
}
// AuditAnnotationCondition contains the inputs needed to compile, evaluate and publish a cel audit annotation
type AuditAnnotationCondition struct {
Key string
ValueExpression string
}
func (v *AuditAnnotationCondition) GetExpression() string {
return v.ValueExpression
}
func (v *AuditAnnotationCondition) ReturnTypes() []*celgo.Type {
return []*celgo.Type{celgo.StringType, celgo.NullType}
}
// Matcher is used for matching ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding to attributes
type Matcher interface {
admission.InitializationValidator
@ -52,9 +72,17 @@ type Matcher interface {
BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error)
}
// ValidateResult defines the result of a Validator.Validate operation.
type ValidateResult struct {
// Decisions specifies the outcome of the validation as well as the details about the decision.
Decisions []PolicyDecision
// AuditAnnotations specifies the audit annotations that should be recorded for the validation.
AuditAnnotations []PolicyAuditAnnotation
}
// Validator is contains logic for converting ValidationEvaluation to PolicyDecisions
type Validator interface {
// Validate is used to take cel evaluations and convert into decisions
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision
Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult
}

View File

@ -47,6 +47,29 @@ type PolicyDecision struct {
Elapsed time.Duration
}
type PolicyAuditAnnotationAction string
const (
// AuditAnnotationActionPublish indicates that the audit annotation should be
// published with the audit event.
AuditAnnotationActionPublish PolicyAuditAnnotationAction = "publish"
// AuditAnnotationActionError indicates that the valueExpression resulted
// in an error.
AuditAnnotationActionError PolicyAuditAnnotationAction = "error"
// AuditAnnotationActionExclude indicates that the audit annotation should be excluded
// because the valueExpression evaluated to null, or because FailurePolicy is Ignore
// and the expression failed with a parse error, type check error, or runtime error.
AuditAnnotationActionExclude PolicyAuditAnnotationAction = "exclude"
)
type PolicyAuditAnnotation struct {
Key string
Value string
Elapsed time.Duration
Action PolicyAuditAnnotationAction
Error string
}
func reasonToCode(r metav1.StatusReason) int32 {
switch r {
case metav1.StatusReasonForbidden:

View File

@ -21,6 +21,7 @@ import (
"strings"
celtypes "github.com/google/cel-go/common/types"
"k8s.io/klog/v2"
v1 "k8s.io/api/admissionregistration/v1"
@ -33,16 +34,18 @@ import (
// validator implements the Validator interface
type validator struct {
filter cel.Filter
failPolicy *v1.FailurePolicyType
authorizer authorizer.Authorizer
validationFilter cel.Filter
auditAnnotationFilter cel.Filter
failPolicy *v1.FailurePolicyType
authorizer authorizer.Authorizer
}
func NewValidator(filter cel.Filter, failPolicy *v1.FailurePolicyType, authorizer authorizer.Authorizer) Validator {
func NewValidator(validationFilter, auditAnnotationFilter cel.Filter, failPolicy *v1.FailurePolicyType, authorizer authorizer.Authorizer) Validator {
return &validator{
filter: filter,
failPolicy: failPolicy,
authorizer: authorizer,
validationFilter: validationFilter,
auditAnnotationFilter: auditAnnotationFilter,
failPolicy: failPolicy,
authorizer: authorizer,
}
}
@ -53,9 +56,16 @@ func policyDecisionActionForError(f v1.FailurePolicyType) PolicyDecisionAction {
return ActionDeny
}
func auditAnnotationEvaluationForError(f v1.FailurePolicyType) PolicyAuditAnnotationAction {
if f == v1.Ignore {
return AuditAnnotationActionExclude
}
return AuditAnnotationActionError
}
// Validate takes a list of Evaluation and a failure policy and converts them into actionable PolicyDecisions
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func (v *validator) Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
func (v *validator) Validate(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
var f v1.FailurePolicyType
if v.failPolicy == nil {
f = v1.Fail
@ -64,13 +74,15 @@ func (v *validator) Validate(versionedAttr *generic.VersionedAttributes, version
}
optionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams, Authorizer: v.authorizer}
evalResults, err := v.filter.ForInput(versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, runtimeCELCostBudget)
evalResults, err := v.validationFilter.ForInput(versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, runtimeCELCostBudget)
if err != nil {
return []PolicyDecision{
{
Action: policyDecisionActionForError(f),
Evaluation: EvalError,
Message: err.Error(),
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: policyDecisionActionForError(f),
Evaluation: EvalError,
Message: err.Error(),
},
},
}
}
@ -104,11 +116,60 @@ func (v *validator) Validate(versionedAttr *generic.VersionedAttributes, version
} else {
decision.Message = fmt.Sprintf("failed expression: %v", strings.TrimSpace(validation.Expression))
}
} else {
decision.Action = ActionAdmit
decision.Evaluation = EvalAdmit
}
}
return decisions
options := cel.OptionalVariableBindings{VersionedParams: versionedParams}
auditAnnotationEvalResults, err := v.auditAnnotationFilter.ForInput(versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), options, runtimeCELCostBudget)
if err != nil {
return ValidateResult{
Decisions: []PolicyDecision{
{
Action: policyDecisionActionForError(f),
Evaluation: EvalError,
Message: err.Error(),
},
},
}
}
auditAnnotationResults := make([]PolicyAuditAnnotation, len(auditAnnotationEvalResults))
for i, evalResult := range auditAnnotationEvalResults {
var auditAnnotationResult = &auditAnnotationResults[i]
// TODO: move this to generics
validation, ok := evalResult.ExpressionAccessor.(*AuditAnnotationCondition)
if !ok {
klog.Error("Invalid type conversion to AuditAnnotationCondition")
auditAnnotationResult.Action = auditAnnotationEvaluationForError(f)
auditAnnotationResult.Error = fmt.Sprintf("Invalid type sent to validator, expected AuditAnnotationCondition but got %T", evalResult.ExpressionAccessor)
continue
}
auditAnnotationResult.Key = validation.Key
if evalResult.Error != nil {
auditAnnotationResult.Action = auditAnnotationEvaluationForError(f)
auditAnnotationResult.Error = evalResult.Error.Error()
} else {
switch evalResult.EvalResult.Type() {
case celtypes.StringType:
value := strings.TrimSpace(evalResult.EvalResult.Value().(string))
if len(value) == 0 {
auditAnnotationResult.Action = AuditAnnotationActionExclude
} else {
auditAnnotationResult.Action = AuditAnnotationActionPublish
auditAnnotationResult.Value = value
}
case celtypes.NullType:
auditAnnotationResult.Action = AuditAnnotationActionExclude
default:
auditAnnotationResult.Action = AuditAnnotationActionError
auditAnnotationResult.Error = fmt.Sprintf("valueExpression '%v' resulted in unsupported return type: %v. "+
"Return type must be either string or null.", validation.ValueExpression, evalResult.EvalResult.Type())
}
}
}
return ValidateResult{Decisions: decisions, AuditAnnotations: auditAnnotationResults}
}

View File

@ -18,6 +18,7 @@ package validatingadmissionpolicy
import (
"errors"
"fmt"
"strings"
"testing"
@ -64,11 +65,13 @@ func TestValidate(t *testing.T) {
fakeVersionedAttr, _ := generic.NewVersionedAttributes(fakeAttr, schema.GroupVersionKind{}, nil)
cases := []struct {
name string
failPolicy *v1.FailurePolicyType
evaluations []cel.EvaluationResult
policyDecision []PolicyDecision
throwError bool
name string
failPolicy *v1.FailurePolicyType
evaluations []cel.EvaluationResult
auditEvaluations []cel.EvaluationResult
policyDecision []PolicyDecision
auditAnnotations []PolicyAuditAnnotation
throwError bool
}{
{
name: "test pass",
@ -455,30 +458,161 @@ func TestValidate(t *testing.T) {
failPolicy: &fail,
throwError: true,
},
{
name: "test empty validations with non-empty audit annotations",
auditEvaluations: []cel.EvaluationResult{
{
EvalResult: celtypes.String("string value"),
ExpressionAccessor: &AuditAnnotationCondition{
ValueExpression: "'string value'",
},
},
},
failPolicy: &fail,
auditAnnotations: []PolicyAuditAnnotation{
{
Action: AuditAnnotationActionPublish,
Value: "string value",
},
},
},
{
name: "test non-empty validations with non-empty audit annotations",
evaluations: []cel.EvaluationResult{
{
EvalResult: celtypes.True,
ExpressionAccessor: &ValidationCondition{
Reason: &forbiddenReason,
Expression: "this.expression == unit.test",
Message: "test1",
},
},
},
auditEvaluations: []cel.EvaluationResult{
{
EvalResult: celtypes.String("string value"),
ExpressionAccessor: &AuditAnnotationCondition{
ValueExpression: "'string value'",
},
},
},
policyDecision: []PolicyDecision{
{
Action: ActionAdmit,
},
},
auditAnnotations: []PolicyAuditAnnotation{
{
Action: AuditAnnotationActionPublish,
Value: "string value",
},
},
failPolicy: &fail,
},
{
name: "test audit annotations with null return",
auditEvaluations: []cel.EvaluationResult{
{
EvalResult: celtypes.NullValue,
ExpressionAccessor: &AuditAnnotationCondition{
ValueExpression: "null",
},
},
{
EvalResult: celtypes.String("string value"),
ExpressionAccessor: &AuditAnnotationCondition{
ValueExpression: "'string value'",
},
},
},
auditAnnotations: []PolicyAuditAnnotation{
{
Action: AuditAnnotationActionExclude,
},
{
Action: AuditAnnotationActionPublish,
Value: "string value",
},
},
failPolicy: &fail,
},
{
name: "test audit annotations with failPolicy=fail",
auditEvaluations: []cel.EvaluationResult{
{
Error: fmt.Errorf("valueExpression ''this is not valid CEL' resulted in error: <nil>"),
ExpressionAccessor: &AuditAnnotationCondition{
ValueExpression: "'this is not valid CEL",
},
},
},
auditAnnotations: []PolicyAuditAnnotation{
{
Action: AuditAnnotationActionError,
Error: "valueExpression ''this is not valid CEL' resulted in error: <nil>",
},
},
failPolicy: &fail,
},
{
name: "test audit annotations with failPolicy=ignore",
auditEvaluations: []cel.EvaluationResult{
{
Error: fmt.Errorf("valueExpression ''this is not valid CEL' resulted in error: <nil>"),
ExpressionAccessor: &AuditAnnotationCondition{
ValueExpression: "'this is not valid CEL",
},
},
},
auditAnnotations: []PolicyAuditAnnotation{
{
Action: AuditAnnotationActionExclude, // TODO: is this right?
Error: "valueExpression ''this is not valid CEL' resulted in error: <nil>",
},
},
failPolicy: &ignore,
},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
v := validator{
failPolicy: tc.failPolicy,
filter: &fakeCelFilter{
validationFilter: &fakeCelFilter{
evaluations: tc.evaluations,
throwError: tc.throwError,
},
auditAnnotationFilter: &fakeCelFilter{
evaluations: tc.auditEvaluations,
throwError: tc.throwError,
},
}
validateResult := v.Validate(fakeVersionedAttr, nil, celconfig.RuntimeCELCostBudget)
policyResults := v.Validate(fakeVersionedAttr, nil, celconfig.RuntimeCELCostBudget)
require.Equal(t, len(policyResults), len(tc.policyDecision))
require.Equal(t, len(validateResult.Decisions), len(tc.policyDecision))
for i, policyDecision := range tc.policyDecision {
if policyDecision.Action != policyResults[i].Action {
t.Errorf("Expected policy decision kind '%v' but got '%v'", policyDecision.Action, policyResults[i].Action)
if policyDecision.Action != validateResult.Decisions[i].Action {
t.Errorf("Expected policy decision kind '%v' but got '%v'", policyDecision.Action, validateResult.Decisions[i].Action)
}
if !strings.Contains(policyResults[i].Message, policyDecision.Message) {
t.Errorf("Expected policy decision message contains '%v' but got '%v'", policyDecision.Message, policyResults[i].Message)
if !strings.Contains(validateResult.Decisions[i].Message, policyDecision.Message) {
t.Errorf("Expected policy decision message contains '%v' but got '%v'", policyDecision.Message, validateResult.Decisions[i].Message)
}
if policyDecision.Reason != policyResults[i].Reason {
t.Errorf("Expected policy decision reason '%v' but got '%v'", policyDecision.Reason, policyResults[i].Reason)
if policyDecision.Reason != validateResult.Decisions[i].Reason {
t.Errorf("Expected policy decision reason '%v' but got '%v'", policyDecision.Reason, validateResult.Decisions[i].Reason)
}
}
require.Equal(t, len(tc.auditEvaluations), len(validateResult.AuditAnnotations))
for i, auditAnnotation := range tc.auditAnnotations {
actual := validateResult.AuditAnnotations[i]
if auditAnnotation.Action != actual.Action {
t.Errorf("Expected policy audit annotation action '%v' but got '%v'", auditAnnotation.Action, actual.Action)
}
if auditAnnotation.Error != actual.Error {
t.Errorf("Expected audit annotation error '%v' but got '%v'", auditAnnotation.Error, actual.Error)
}
if auditAnnotation.Value != actual.Value {
t.Errorf("Expected policy audit annotation value '%v' but got '%v'", auditAnnotation.Value, actual.Value)
}
}
})