mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 02:41:25 +00:00
Implement validationActions and auditAnnotations
This commit is contained in:
parent
f461527491
commit
d221ddb89a
@ -154,18 +154,35 @@ type ValidatingAdmissionPolicySpec struct {
|
|||||||
// Required.
|
// Required.
|
||||||
MatchConstraints *MatchResources
|
MatchConstraints *MatchResources
|
||||||
|
|
||||||
// Validations contain CEL expressions which is used to apply the validation.
|
// validations contain CEL expressions which are used to validate admission requests.
|
||||||
// A minimum of one validation is required for a policy definition.
|
// validations and auditAnnotations may not both be empty; a minimum of one validations or auditAnnotations is
|
||||||
// Required.
|
// required.
|
||||||
|
// +optional
|
||||||
Validations []Validation
|
Validations []Validation
|
||||||
|
|
||||||
// FailurePolicy defines how to handle failures for the admission policy.
|
// failurePolicy defines how to handle failures for the admission policy. Failures can
|
||||||
// Failures can occur from invalid or mis-configured policy definitions or bindings.
|
// occur from CEL expression parse errors, type check errors, runtime errors and invalid
|
||||||
|
// or mis-configured policy definitions or bindings.
|
||||||
|
//
|
||||||
// A policy is invalid if spec.paramKind refers to a non-existent Kind.
|
// A policy is invalid if spec.paramKind refers to a non-existent Kind.
|
||||||
// A binding is invalid if spec.paramRef.name refers to a non-existent resource.
|
// A binding is invalid if spec.paramRef.name refers to a non-existent resource.
|
||||||
|
//
|
||||||
|
// failurePolicy does not define how validations that evaluate to false are handled.
|
||||||
|
//
|
||||||
|
// When failurePolicy is set to Fail, ValidatingAdmissionPolicyBinding validationActions
|
||||||
|
// define how failures are enforced.
|
||||||
|
//
|
||||||
// Allowed values are Ignore or Fail. Defaults to Fail.
|
// Allowed values are Ignore or Fail. Defaults to Fail.
|
||||||
// +optional
|
// +optional
|
||||||
FailurePolicy *FailurePolicyType
|
FailurePolicy *FailurePolicyType
|
||||||
|
|
||||||
|
// auditAnnotations contains CEL expressions which are used to produce audit
|
||||||
|
// annotations for the audit event of the API request.
|
||||||
|
// validations and auditAnnotations may not both be empty; a least one of validations or auditAnnotations is
|
||||||
|
// required.
|
||||||
|
// A maximum of 20 auditAnnotation are allowed per ValidatingAdmissionPolicy.
|
||||||
|
// +optional
|
||||||
|
AuditAnnotations []AuditAnnotation
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParamKind is a tuple of Group Kind and Version.
|
// ParamKind is a tuple of Group Kind and Version.
|
||||||
@ -184,12 +201,12 @@ type ParamKind struct {
|
|||||||
type Validation struct {
|
type Validation struct {
|
||||||
// Expression represents the expression which will be evaluated by CEL.
|
// Expression represents the expression which will be evaluated by CEL.
|
||||||
// ref: https://github.com/google/cel-spec
|
// ref: https://github.com/google/cel-spec
|
||||||
// CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables:
|
// CEL expressions have access to the contents of the API request/response, organized into CEL variables as well as some other useful variables:
|
||||||
//
|
//
|
||||||
// - 'object' - The object from the incoming request. The value is null for DELETE requests.
|
//'object' - The object from the incoming request. The value is null for DELETE requests.
|
||||||
// - 'oldObject' - The existing object. The value is null for CREATE requests.
|
//'oldObject' - The existing object. The value is null for CREATE requests.
|
||||||
// - 'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)).
|
//'request' - Attributes of the API request([ref](/pkg/apis/admission/types.go#AdmissionRequest)).
|
||||||
// - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.
|
//'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.
|
||||||
// - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.
|
// - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.
|
||||||
// See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz
|
// See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz
|
||||||
// - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the
|
// - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the
|
||||||
@ -241,6 +258,43 @@ type Validation struct {
|
|||||||
Reason *metav1.StatusReason
|
Reason *metav1.StatusReason
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuditAnnotation describes how to produce an audit annotation for an API request.
|
||||||
|
type AuditAnnotation struct {
|
||||||
|
// key specifies the audit annotation key. The audit annotation keys of
|
||||||
|
// a ValidatingAdmissionPolicy must be unique. The key must be a qualified
|
||||||
|
// name ([A-Za-z0-9][-A-Za-z0-9_.]*) no more than 63 bytes in length.
|
||||||
|
//
|
||||||
|
// The key is combined with the resource name of the
|
||||||
|
// ValidatingAdmissionPolicy to construct an audit annotation key:
|
||||||
|
// "{ValidatingAdmissionPolicy name}/{key}".
|
||||||
|
//
|
||||||
|
// If an admission webhook uses the same resource name as this ValidatingAdmissionPolicy
|
||||||
|
// and the same audit annotation key, the annotation key will be identical.
|
||||||
|
// In this case, the first annotation written with the key will be included
|
||||||
|
// in the audit event and all subsequent annotations with the same key
|
||||||
|
// will be discarded.
|
||||||
|
//
|
||||||
|
// Required.
|
||||||
|
Key string
|
||||||
|
|
||||||
|
// valueExpression represents the expression which is evaluated by CEL to
|
||||||
|
// produce an audit annotation value. The expression must evaluate to either
|
||||||
|
// a string or null value. If the expression evaluates to a string, the
|
||||||
|
// audit annotation is included with the string value. If the expression
|
||||||
|
// evaluates to null or empty string the audit annotation will be omitted.
|
||||||
|
// The valueExpression may be no longer than 5kb in length.
|
||||||
|
// If the result of the valueExpression is more than 10kb in length, it
|
||||||
|
// will be truncated to 10kb.
|
||||||
|
//
|
||||||
|
// If multiple ValidatingAdmissionPolicyBinding resources match an
|
||||||
|
// API request, then the valueExpression will be evaluated for
|
||||||
|
// each binding. All unique values produced by the valueExpressions
|
||||||
|
// will be joined together in a comma-separated list.
|
||||||
|
//
|
||||||
|
// Required.
|
||||||
|
ValueExpression string
|
||||||
|
}
|
||||||
|
|
||||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
|
|
||||||
// ValidatingAdmissionPolicyBinding binds the ValidatingAdmissionPolicy with paramerized resources.
|
// ValidatingAdmissionPolicyBinding binds the ValidatingAdmissionPolicy with paramerized resources.
|
||||||
@ -287,6 +341,47 @@ type ValidatingAdmissionPolicyBindingSpec struct {
|
|||||||
// Note that this is differs from ValidatingAdmissionPolicy matchConstraints, where resourceRules are required.
|
// Note that this is differs from ValidatingAdmissionPolicy matchConstraints, where resourceRules are required.
|
||||||
// +optional
|
// +optional
|
||||||
MatchResources *MatchResources
|
MatchResources *MatchResources
|
||||||
|
|
||||||
|
// validationActions declares how Validations of the referenced ValidatingAdmissionPolicy are enforced.
|
||||||
|
// If a validation evaluates to false it is always enforced according to these actions.
|
||||||
|
//
|
||||||
|
// Failures defined by the ValidatingAdmissionPolicy's FailurePolicy are enforced according
|
||||||
|
// to these actions only if the FailurePolicy is set to Fail, otherwise the failures are
|
||||||
|
// ignored. This includes compilation errors, runtime errors and misconfigurations of the policy.
|
||||||
|
//
|
||||||
|
// validationActions is declared as a set of action values. Order does
|
||||||
|
// not matter. validationActions may not contain duplicates of the same action.
|
||||||
|
//
|
||||||
|
// The supported actions values are:
|
||||||
|
//
|
||||||
|
// "Deny" specifies that a validation failure results in a denied request.
|
||||||
|
//
|
||||||
|
// "Warn" specifies that a validation failure is reported to the request client
|
||||||
|
// in HTTP Warning headers, with a warning code of 299. Warnings can be sent
|
||||||
|
// both for allowed or denied admission responses.
|
||||||
|
//
|
||||||
|
// "Audit" specifies that a validation failure is included in the published
|
||||||
|
// audit event for the request. The audit event will contain a
|
||||||
|
// `validation.policy.admission.k8s.io/validation_failure` audit annotation
|
||||||
|
// with a value containing the details of the validation failures, formatted as
|
||||||
|
// a JSON list of objects, each with the following fields:
|
||||||
|
// - message: The validation failure message string
|
||||||
|
// - policy: The resource name of the ValidatingAdmissionPolicy
|
||||||
|
// - binding: The resource name of the ValidatingAdmissionPolicyBinding
|
||||||
|
// - expressionIndex: The index of the failed validations in the ValidatingAdmissionPolicy
|
||||||
|
// - validationActions: The enforcement actions enacted for the validation failure
|
||||||
|
// Example audit annotation:
|
||||||
|
// `"validation.policy.admission.k8s.io/validation_failure": "[{\"message\": \"Invalid value\", {\"policy\": \"policy.example.com\", {\"binding\": \"policybinding.example.com\", {\"expressionIndex\": \"1\", {\"validationActions\": [\"Audit\"]}]"`
|
||||||
|
//
|
||||||
|
// Clients should expect to handle additional values by ignoring
|
||||||
|
// any values not recognized.
|
||||||
|
//
|
||||||
|
// "Deny" and "Warn" may not be used together since this combination
|
||||||
|
// needlessly duplicates the validation failure both in the
|
||||||
|
// API response body and the HTTP warning headers.
|
||||||
|
//
|
||||||
|
// Required.
|
||||||
|
ValidationActions []ValidationAction
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParamRef references a parameter resource
|
// ParamRef references a parameter resource
|
||||||
@ -387,6 +482,23 @@ type MatchResources struct {
|
|||||||
MatchPolicy *MatchPolicyType
|
MatchPolicy *MatchPolicyType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidationAction specifies a policy enforcement action.
|
||||||
|
type ValidationAction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Deny specifies that a validation failure results in a denied request.
|
||||||
|
Deny ValidationAction = "Deny"
|
||||||
|
// Warn specifies that a validation failure is reported to the request client
|
||||||
|
// in HTTP Warning headers, with a warning code of 299. Warnings can be sent
|
||||||
|
// both for allowed or denied admission responses.
|
||||||
|
Warn ValidationAction = "Warn"
|
||||||
|
// Audit specifies that a validation failure is included in the published
|
||||||
|
// audit event for the request. The audit event will contain a
|
||||||
|
// `validation.policy.admission.k8s.io/validation_failure` audit annotation
|
||||||
|
// with a value containing the details of the validation failure.
|
||||||
|
Audit ValidationAction = "Audit"
|
||||||
|
)
|
||||||
|
|
||||||
// NamedRuleWithOperations is a tuple of Operations and Resources with ResourceNames.
|
// NamedRuleWithOperations is a tuple of Operations and Resources with ResourceNames.
|
||||||
type NamedRuleWithOperations struct {
|
type NamedRuleWithOperations struct {
|
||||||
// ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed.
|
// ResourceNames is an optional white list of names that the rule applies to. An empty set means that everything is allowed.
|
||||||
|
@ -37,6 +37,7 @@ import (
|
|||||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||||
admissionregistrationv1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1"
|
admissionregistrationv1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1"
|
||||||
admissionregistrationv1beta1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1beta1"
|
admissionregistrationv1beta1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1beta1"
|
||||||
|
apivalidation "k8s.io/kubernetes/pkg/apis/core/validation"
|
||||||
)
|
)
|
||||||
|
|
||||||
func hasWildcard(slice []string) bool {
|
func hasWildcard(slice []string) bool {
|
||||||
@ -583,6 +584,14 @@ func ValidateMutatingWebhookConfigurationUpdate(newC, oldC *admissionregistratio
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
maxAuditAnnotations = 20
|
||||||
|
// use a 5kb limit the CEL expression, note that this is less than the length limit
|
||||||
|
// for the audit annotation value limit (10kb) since an expressions that concatenates
|
||||||
|
// strings will often produce a longer value than the expression
|
||||||
|
maxAuditAnnotationValueExpressionLength = 5 * 1024
|
||||||
|
)
|
||||||
|
|
||||||
// ValidateValidatingAdmissionPolicy validates a ValidatingAdmissionPolicy before creation.
|
// ValidateValidatingAdmissionPolicy validates a ValidatingAdmissionPolicy before creation.
|
||||||
func ValidateValidatingAdmissionPolicy(p *admissionregistration.ValidatingAdmissionPolicy) field.ErrorList {
|
func ValidateValidatingAdmissionPolicy(p *admissionregistration.ValidatingAdmissionPolicy) field.ErrorList {
|
||||||
return validateValidatingAdmissionPolicy(p)
|
return validateValidatingAdmissionPolicy(p)
|
||||||
@ -590,11 +599,11 @@ func ValidateValidatingAdmissionPolicy(p *admissionregistration.ValidatingAdmiss
|
|||||||
|
|
||||||
func validateValidatingAdmissionPolicy(p *admissionregistration.ValidatingAdmissionPolicy) field.ErrorList {
|
func validateValidatingAdmissionPolicy(p *admissionregistration.ValidatingAdmissionPolicy) field.ErrorList {
|
||||||
allErrors := genericvalidation.ValidateObjectMeta(&p.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))
|
allErrors := genericvalidation.ValidateObjectMeta(&p.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))
|
||||||
allErrors = append(allErrors, validateValidatingAdmissionPolicySpec(&p.Spec, field.NewPath("spec"))...)
|
allErrors = append(allErrors, validateValidatingAdmissionPolicySpec(p.ObjectMeta, &p.Spec, field.NewPath("spec"))...)
|
||||||
return allErrors
|
return allErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateValidatingAdmissionPolicySpec(spec *admissionregistration.ValidatingAdmissionPolicySpec, fldPath *field.Path) field.ErrorList {
|
func validateValidatingAdmissionPolicySpec(meta metav1.ObjectMeta, spec *admissionregistration.ValidatingAdmissionPolicySpec, fldPath *field.Path) field.ErrorList {
|
||||||
var allErrors field.ErrorList
|
var allErrors field.ErrorList
|
||||||
if spec.FailurePolicy == nil {
|
if spec.FailurePolicy == nil {
|
||||||
allErrors = append(allErrors, field.Required(fldPath.Child("failurePolicy"), ""))
|
allErrors = append(allErrors, field.Required(fldPath.Child("failurePolicy"), ""))
|
||||||
@ -613,14 +622,27 @@ func validateValidatingAdmissionPolicySpec(spec *admissionregistration.Validatin
|
|||||||
allErrors = append(allErrors, field.Required(fldPath.Child("matchConstraints", "resourceRules"), ""))
|
allErrors = append(allErrors, field.Required(fldPath.Child("matchConstraints", "resourceRules"), ""))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(spec.Validations) == 0 {
|
if len(spec.Validations) == 0 && len(spec.AuditAnnotations) == 0 {
|
||||||
allErrors = append(allErrors, field.Required(fldPath.Child("validations"), ""))
|
allErrors = append(allErrors, field.Required(fldPath.Child("validations"), "validations or auditAnnotations must contain at least one item"))
|
||||||
|
allErrors = append(allErrors, field.Required(fldPath.Child("auditAnnotations"), "validations or auditAnnotations must contain at least one item"))
|
||||||
} else {
|
} else {
|
||||||
for i, validation := range spec.Validations {
|
for i, validation := range spec.Validations {
|
||||||
allErrors = append(allErrors, validateValidation(&validation, spec.ParamKind, fldPath.Child("validations").Index(i))...)
|
allErrors = append(allErrors, validateValidation(&validation, spec.ParamKind, fldPath.Child("validations").Index(i))...)
|
||||||
}
|
}
|
||||||
|
if spec.AuditAnnotations != nil {
|
||||||
|
keys := sets.NewString()
|
||||||
|
if len(spec.AuditAnnotations) > maxAuditAnnotations {
|
||||||
|
allErrors = append(allErrors, field.Invalid(fldPath.Child("auditAnnotations"), spec.AuditAnnotations, fmt.Sprintf("must not have more than %d auditAnnotations", maxAuditAnnotations)))
|
||||||
|
}
|
||||||
|
for i, auditAnnotation := range spec.AuditAnnotations {
|
||||||
|
allErrors = append(allErrors, validateAuditAnnotation(meta, &auditAnnotation, spec.ParamKind, fldPath.Child("auditAnnotations").Index(i))...)
|
||||||
|
if keys.Has(auditAnnotation.Key) {
|
||||||
|
allErrors = append(allErrors, field.Duplicate(fldPath.Child("auditAnnotations").Index(i).Child("key"), auditAnnotation.Key))
|
||||||
|
}
|
||||||
|
keys.Insert(auditAnnotation.Key)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return allErrors
|
return allErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -712,6 +734,33 @@ func validateMatchResources(mc *admissionregistration.MatchResources, fldPath *f
|
|||||||
return allErrors
|
return allErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var validValidationActions = sets.NewString(
|
||||||
|
string(admissionregistration.Deny),
|
||||||
|
string(admissionregistration.Warn),
|
||||||
|
string(admissionregistration.Audit),
|
||||||
|
)
|
||||||
|
|
||||||
|
func validateValidationActions(va []admissionregistration.ValidationAction, fldPath *field.Path) field.ErrorList {
|
||||||
|
var allErrors field.ErrorList
|
||||||
|
actions := sets.NewString()
|
||||||
|
for i, action := range va {
|
||||||
|
if !validValidationActions.Has(string(action)) {
|
||||||
|
allErrors = append(allErrors, field.NotSupported(fldPath.Index(i), action, validValidationActions.List()))
|
||||||
|
}
|
||||||
|
if actions.Has(string(action)) {
|
||||||
|
allErrors = append(allErrors, field.Duplicate(fldPath.Index(i), action))
|
||||||
|
}
|
||||||
|
actions.Insert(string(action))
|
||||||
|
}
|
||||||
|
if actions.Has(string(admissionregistration.Deny)) && actions.Has(string(admissionregistration.Warn)) {
|
||||||
|
allErrors = append(allErrors, field.Invalid(fldPath, va, "must not contain both Deny and Warn (repeating the same validation failure information in the API response and headers serves no purpose)"))
|
||||||
|
}
|
||||||
|
if len(actions) == 0 {
|
||||||
|
allErrors = append(allErrors, field.Required(fldPath, "at least one validation action is required"))
|
||||||
|
}
|
||||||
|
return allErrors
|
||||||
|
}
|
||||||
|
|
||||||
func validateNamedRuleWithOperations(n *admissionregistration.NamedRuleWithOperations, fldPath *field.Path) field.ErrorList {
|
func validateNamedRuleWithOperations(n *admissionregistration.NamedRuleWithOperations, fldPath *field.Path) field.ErrorList {
|
||||||
var allErrors field.ErrorList
|
var allErrors field.ErrorList
|
||||||
resourceNames := sets.NewString()
|
resourceNames := sets.NewString()
|
||||||
@ -767,6 +816,38 @@ func validateValidation(v *admissionregistration.Validation, paramKind *admissio
|
|||||||
return allErrors
|
return allErrors
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func validateAuditAnnotation(meta metav1.ObjectMeta, v *admissionregistration.AuditAnnotation, paramKind *admissionregistration.ParamKind, fldPath *field.Path) field.ErrorList {
|
||||||
|
var allErrors field.ErrorList
|
||||||
|
if len(meta.GetName()) != 0 {
|
||||||
|
name := meta.GetName()
|
||||||
|
allErrors = append(allErrors, apivalidation.ValidateQualifiedName(name+"/"+v.Key, fldPath.Child("key"))...)
|
||||||
|
} else {
|
||||||
|
allErrors = append(allErrors, field.Invalid(fldPath.Child("key"), v.Key, "requires metadata.name be non-empty"))
|
||||||
|
}
|
||||||
|
|
||||||
|
trimmedValueExpression := strings.TrimSpace(v.ValueExpression)
|
||||||
|
if len(trimmedValueExpression) == 0 {
|
||||||
|
allErrors = append(allErrors, field.Required(fldPath.Child("valueExpression"), "valueExpression is not specified"))
|
||||||
|
} else if len(trimmedValueExpression) > maxAuditAnnotationValueExpressionLength {
|
||||||
|
allErrors = append(allErrors, field.Required(fldPath.Child("valueExpression"), fmt.Sprintf("must not exceed %d bytes in length", maxAuditAnnotationValueExpressionLength)))
|
||||||
|
} else {
|
||||||
|
result := plugincel.CompileCELExpression(&validatingadmissionpolicy.AuditAnnotationCondition{
|
||||||
|
ValueExpression: trimmedValueExpression,
|
||||||
|
}, plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true}, celconfig.PerCallLimit)
|
||||||
|
if result.Error != nil {
|
||||||
|
switch result.Error.Type {
|
||||||
|
case cel.ErrorTypeRequired:
|
||||||
|
allErrors = append(allErrors, field.Required(fldPath.Child("valueExpression"), result.Error.Detail))
|
||||||
|
case cel.ErrorTypeInvalid:
|
||||||
|
allErrors = append(allErrors, field.Invalid(fldPath.Child("valueExpression"), v.ValueExpression, result.Error.Detail))
|
||||||
|
default:
|
||||||
|
allErrors = append(allErrors, field.InternalError(fldPath.Child("valueExpression"), result.Error))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return allErrors
|
||||||
|
}
|
||||||
|
|
||||||
var newlineMatcher = regexp.MustCompile(`[\n\r]+`) // valid newline chars in CEL grammar
|
var newlineMatcher = regexp.MustCompile(`[\n\r]+`) // valid newline chars in CEL grammar
|
||||||
func hasNewlines(s string) bool {
|
func hasNewlines(s string) bool {
|
||||||
return newlineMatcher.MatchString(s)
|
return newlineMatcher.MatchString(s)
|
||||||
@ -796,6 +877,7 @@ func validateValidatingAdmissionPolicyBindingSpec(spec *admissionregistration.Va
|
|||||||
}
|
}
|
||||||
allErrors = append(allErrors, validateParamRef(spec.ParamRef, fldPath.Child("paramRef"))...)
|
allErrors = append(allErrors, validateParamRef(spec.ParamRef, fldPath.Child("paramRef"))...)
|
||||||
allErrors = append(allErrors, validateMatchResources(spec.MatchResources, fldPath.Child("matchResouces"))...)
|
allErrors = append(allErrors, validateMatchResources(spec.MatchResources, fldPath.Child("matchResouces"))...)
|
||||||
|
allErrors = append(allErrors, validateValidationActions(spec.ValidationActions, fldPath.Child("validationActions"))...)
|
||||||
|
|
||||||
return allErrors
|
return allErrors
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
|
||||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -1999,7 +2000,7 @@ func TestValidateValidatingAdmissionPolicy(t *testing.T) {
|
|||||||
Spec: admissionregistration.ValidatingAdmissionPolicySpec{},
|
Spec: admissionregistration.ValidatingAdmissionPolicySpec{},
|
||||||
},
|
},
|
||||||
|
|
||||||
expectedError: `spec.validations: Required value`,
|
expectedError: `spec.validations: Required value: validations or auditAnnotations must contain at least one item`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "Invalid Validations Reason",
|
name: "Invalid Validations Reason",
|
||||||
@ -2565,6 +2566,112 @@ func TestValidateValidatingAdmissionPolicy(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedError: `spec.validations[0].expression: Invalid value: "object.x in [1, 2, ": compilation failed: ERROR: <input>:1:19: Syntax error: missing ']' at '<EOF>`,
|
expectedError: `spec.validations[0].expression: Invalid value: "object.x in [1, 2, ": compilation failed: ERROR: <input>:1:19: Syntax error: missing ']' at '<EOF>`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "invalid auditAnnotations key due to key name",
|
||||||
|
config: &admissionregistration.ValidatingAdmissionPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "config",
|
||||||
|
},
|
||||||
|
Spec: admissionregistration.ValidatingAdmissionPolicySpec{
|
||||||
|
AuditAnnotations: []admissionregistration.AuditAnnotation{
|
||||||
|
{
|
||||||
|
Key: "@",
|
||||||
|
ValueExpression: "value",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: `spec.auditAnnotations[0].key: Invalid value: "config/@": name part must consist of alphanumeric characters`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "auditAnnotations keys must be unique",
|
||||||
|
config: &admissionregistration.ValidatingAdmissionPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "config",
|
||||||
|
},
|
||||||
|
Spec: admissionregistration.ValidatingAdmissionPolicySpec{
|
||||||
|
AuditAnnotations: []admissionregistration.AuditAnnotation{
|
||||||
|
{
|
||||||
|
Key: "a",
|
||||||
|
ValueExpression: "'1'",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Key: "a",
|
||||||
|
ValueExpression: "'2'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: `spec.auditAnnotations[1].key: Duplicate value: "a"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid auditAnnotations key due to metadata.name",
|
||||||
|
config: &admissionregistration.ValidatingAdmissionPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "nope!",
|
||||||
|
},
|
||||||
|
Spec: admissionregistration.ValidatingAdmissionPolicySpec{
|
||||||
|
AuditAnnotations: []admissionregistration.AuditAnnotation{
|
||||||
|
{
|
||||||
|
Key: "key",
|
||||||
|
ValueExpression: "'value'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: `spec.auditAnnotations[0].key: Invalid value: "nope!/key": prefix part a lowercase RFC 1123 subdomain`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid auditAnnotations key due to length",
|
||||||
|
config: &admissionregistration.ValidatingAdmissionPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "this-is-a-long-name-for-a-admission-policy-xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
|
},
|
||||||
|
Spec: admissionregistration.ValidatingAdmissionPolicySpec{
|
||||||
|
AuditAnnotations: []admissionregistration.AuditAnnotation{
|
||||||
|
{
|
||||||
|
Key: "this-is-a-long-name-for-an-audit-annotation-key-xxxxxxxxxxxxxxxxxxxxxxxxxx",
|
||||||
|
ValueExpression: "'value'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: `spec.auditAnnotations[0].key: Invalid value`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid auditAnnotations valueExpression type",
|
||||||
|
config: &admissionregistration.ValidatingAdmissionPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "config",
|
||||||
|
},
|
||||||
|
Spec: admissionregistration.ValidatingAdmissionPolicySpec{
|
||||||
|
AuditAnnotations: []admissionregistration.AuditAnnotation{
|
||||||
|
{
|
||||||
|
Key: "something",
|
||||||
|
ValueExpression: "true",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: `spec.auditAnnotations[0].valueExpression: Invalid value: "true": must evaluate to one of [string null_type]`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid auditAnnotations valueExpression",
|
||||||
|
config: &admissionregistration.ValidatingAdmissionPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "config",
|
||||||
|
},
|
||||||
|
Spec: admissionregistration.ValidatingAdmissionPolicySpec{
|
||||||
|
AuditAnnotations: []admissionregistration.AuditAnnotation{
|
||||||
|
{
|
||||||
|
Key: "something",
|
||||||
|
ValueExpression: "object.x in [1, 2, ",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: `spec.auditAnnotations[0].valueExpression: Invalid value: "object.x in [1, 2, ": compilation failed: ERROR: <input>:1:19: Syntax error: missing ']' at '<EOF>`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
@ -2580,7 +2687,6 @@ func TestValidateValidatingAdmissionPolicy(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2709,6 +2815,7 @@ func TestValidateValidatingAdmissionPolicyUpdate(t *testing.T) {
|
|||||||
Spec: admissionregistration.ValidatingAdmissionPolicySpec{},
|
Spec: admissionregistration.ValidatingAdmissionPolicySpec{},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// TODO: CustomAuditAnnotations: string valueExpression with {oldObject} is allowed
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
@ -2902,6 +3009,7 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) {
|
|||||||
ParamRef: &admissionregistration.ParamRef{
|
ParamRef: &admissionregistration.ParamRef{
|
||||||
Name: "xyzlimit-scale-setting.example.com",
|
Name: "xyzlimit-scale-setting.example.com",
|
||||||
},
|
},
|
||||||
|
ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny},
|
||||||
MatchResources: &admissionregistration.MatchResources{
|
MatchResources: &admissionregistration.MatchResources{
|
||||||
ResourceRules: []admissionregistration.NamedRuleWithOperations{
|
ResourceRules: []admissionregistration.NamedRuleWithOperations{
|
||||||
{
|
{
|
||||||
@ -2931,6 +3039,7 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) {
|
|||||||
ParamRef: &admissionregistration.ParamRef{
|
ParamRef: &admissionregistration.ParamRef{
|
||||||
Name: "xyzlimit-scale-setting.example.com",
|
Name: "xyzlimit-scale-setting.example.com",
|
||||||
},
|
},
|
||||||
|
ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny},
|
||||||
MatchResources: &admissionregistration.MatchResources{
|
MatchResources: &admissionregistration.MatchResources{
|
||||||
NamespaceSelector: &metav1.LabelSelector{
|
NamespaceSelector: &metav1.LabelSelector{
|
||||||
MatchLabels: map[string]string{"a": "b"},
|
MatchLabels: map[string]string{"a": "b"},
|
||||||
@ -2969,6 +3078,7 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) {
|
|||||||
ParamRef: &admissionregistration.ParamRef{
|
ParamRef: &admissionregistration.ParamRef{
|
||||||
Name: "xyzlimit-scale-setting.example.com",
|
Name: "xyzlimit-scale-setting.example.com",
|
||||||
},
|
},
|
||||||
|
ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny},
|
||||||
MatchResources: &admissionregistration.MatchResources{
|
MatchResources: &admissionregistration.MatchResources{
|
||||||
ResourceRules: []admissionregistration.NamedRuleWithOperations{
|
ResourceRules: []admissionregistration.NamedRuleWithOperations{
|
||||||
{
|
{
|
||||||
@ -2998,6 +3108,7 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) {
|
|||||||
ParamRef: &admissionregistration.ParamRef{
|
ParamRef: &admissionregistration.ParamRef{
|
||||||
Name: "xyzlimit-scale-setting.example.com",
|
Name: "xyzlimit-scale-setting.example.com",
|
||||||
},
|
},
|
||||||
|
ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny},
|
||||||
MatchResources: &admissionregistration.MatchResources{
|
MatchResources: &admissionregistration.MatchResources{
|
||||||
ResourceRules: []admissionregistration.NamedRuleWithOperations{
|
ResourceRules: []admissionregistration.NamedRuleWithOperations{
|
||||||
{
|
{
|
||||||
@ -3027,6 +3138,7 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) {
|
|||||||
ParamRef: &admissionregistration.ParamRef{
|
ParamRef: &admissionregistration.ParamRef{
|
||||||
Name: "xyzlimit-scale-setting.example.com",
|
Name: "xyzlimit-scale-setting.example.com",
|
||||||
},
|
},
|
||||||
|
ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny},
|
||||||
MatchResources: &admissionregistration.MatchResources{
|
MatchResources: &admissionregistration.MatchResources{
|
||||||
NamespaceSelector: &metav1.LabelSelector{
|
NamespaceSelector: &metav1.LabelSelector{
|
||||||
MatchLabels: map[string]string{"a": "b"},
|
MatchLabels: map[string]string{"a": "b"},
|
||||||
@ -3065,6 +3177,7 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) {
|
|||||||
ParamRef: &admissionregistration.ParamRef{
|
ParamRef: &admissionregistration.ParamRef{
|
||||||
Name: "xyzlimit-scale-setting.example.com",
|
Name: "xyzlimit-scale-setting.example.com",
|
||||||
},
|
},
|
||||||
|
ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny},
|
||||||
MatchResources: &admissionregistration.MatchResources{
|
MatchResources: &admissionregistration.MatchResources{
|
||||||
ResourceRules: []admissionregistration.NamedRuleWithOperations{
|
ResourceRules: []admissionregistration.NamedRuleWithOperations{
|
||||||
{
|
{
|
||||||
@ -3094,6 +3207,7 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) {
|
|||||||
ParamRef: &admissionregistration.ParamRef{
|
ParamRef: &admissionregistration.ParamRef{
|
||||||
Name: "xyzlimit-scale-setting.example.com",
|
Name: "xyzlimit-scale-setting.example.com",
|
||||||
},
|
},
|
||||||
|
ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny},
|
||||||
MatchResources: &admissionregistration.MatchResources{
|
MatchResources: &admissionregistration.MatchResources{
|
||||||
ResourceRules: []admissionregistration.NamedRuleWithOperations{
|
ResourceRules: []admissionregistration.NamedRuleWithOperations{
|
||||||
{
|
{
|
||||||
@ -3112,6 +3226,38 @@ func TestValidateValidatingAdmissionPolicyBinding(t *testing.T) {
|
|||||||
},
|
},
|
||||||
expectedError: `spec.matchResouces.resourceRules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`,
|
expectedError: `spec.matchResouces.resourceRules[0].resources: Invalid value: []string{"*/*", "a"}: if '*/*' is present, must not specify other resources`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "validationActions must be unique",
|
||||||
|
config: &admissionregistration.ValidatingAdmissionPolicyBinding{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "config",
|
||||||
|
},
|
||||||
|
Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{
|
||||||
|
PolicyName: "xyzlimit-scale.example.com",
|
||||||
|
ParamRef: &admissionregistration.ParamRef{
|
||||||
|
Name: "xyzlimit-scale-setting.example.com",
|
||||||
|
},
|
||||||
|
ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny, admissionregistration.Deny},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: `spec.validationActions[1]: Duplicate value: "Deny"`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "validationActions must contain supported values",
|
||||||
|
config: &admissionregistration.ValidatingAdmissionPolicyBinding{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "config",
|
||||||
|
},
|
||||||
|
Spec: admissionregistration.ValidatingAdmissionPolicyBindingSpec{
|
||||||
|
PolicyName: "xyzlimit-scale.example.com",
|
||||||
|
ParamRef: &admissionregistration.ParamRef{
|
||||||
|
Name: "xyzlimit-scale-setting.example.com",
|
||||||
|
},
|
||||||
|
ValidationActions: []admissionregistration.ValidationAction{admissionregistration.ValidationAction("illegal")},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectedError: `Unsupported value: "illegal": supported values: "Audit", "Deny", "Warn"`,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
for _, test := range tests {
|
for _, test := range tests {
|
||||||
t.Run(test.name, func(t *testing.T) {
|
t.Run(test.name, func(t *testing.T) {
|
||||||
@ -3149,6 +3295,7 @@ func TestValidateValidatingAdmissionPolicyBindingUpdate(t *testing.T) {
|
|||||||
ParamRef: &admissionregistration.ParamRef{
|
ParamRef: &admissionregistration.ParamRef{
|
||||||
Name: "xyzlimit-scale-setting.example.com",
|
Name: "xyzlimit-scale-setting.example.com",
|
||||||
},
|
},
|
||||||
|
ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny},
|
||||||
MatchResources: &admissionregistration.MatchResources{
|
MatchResources: &admissionregistration.MatchResources{
|
||||||
NamespaceSelector: &metav1.LabelSelector{
|
NamespaceSelector: &metav1.LabelSelector{
|
||||||
MatchLabels: map[string]string{"a": "b"},
|
MatchLabels: map[string]string{"a": "b"},
|
||||||
@ -3184,6 +3331,7 @@ func TestValidateValidatingAdmissionPolicyBindingUpdate(t *testing.T) {
|
|||||||
ParamRef: &admissionregistration.ParamRef{
|
ParamRef: &admissionregistration.ParamRef{
|
||||||
Name: "xyzlimit-scale-setting.example.com",
|
Name: "xyzlimit-scale-setting.example.com",
|
||||||
},
|
},
|
||||||
|
ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny},
|
||||||
MatchResources: &admissionregistration.MatchResources{
|
MatchResources: &admissionregistration.MatchResources{
|
||||||
NamespaceSelector: &metav1.LabelSelector{
|
NamespaceSelector: &metav1.LabelSelector{
|
||||||
MatchLabels: map[string]string{"a": "b"},
|
MatchLabels: map[string]string{"a": "b"},
|
||||||
@ -3222,6 +3370,7 @@ func TestValidateValidatingAdmissionPolicyBindingUpdate(t *testing.T) {
|
|||||||
ParamRef: &admissionregistration.ParamRef{
|
ParamRef: &admissionregistration.ParamRef{
|
||||||
Name: "xyzlimit-scale-setting.example.com",
|
Name: "xyzlimit-scale-setting.example.com",
|
||||||
},
|
},
|
||||||
|
ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny},
|
||||||
MatchResources: &admissionregistration.MatchResources{
|
MatchResources: &admissionregistration.MatchResources{
|
||||||
NamespaceSelector: &metav1.LabelSelector{
|
NamespaceSelector: &metav1.LabelSelector{
|
||||||
MatchLabels: map[string]string{"a": "b"},
|
MatchLabels: map[string]string{"a": "b"},
|
||||||
|
@ -27,6 +27,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/registry/generic"
|
"k8s.io/apiserver/pkg/registry/generic"
|
||||||
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
|
||||||
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
|
||||||
|
|
||||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||||
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
|
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
|
||||||
"k8s.io/kubernetes/pkg/registry/registrytest"
|
"k8s.io/kubernetes/pkg/registry/registrytest"
|
||||||
@ -127,6 +128,7 @@ func validPolicyBinding() *admissionregistration.ValidatingAdmissionPolicyBindin
|
|||||||
ParamRef: &admissionregistration.ParamRef{
|
ParamRef: &admissionregistration.ParamRef{
|
||||||
Name: "param-test",
|
Name: "param-test",
|
||||||
},
|
},
|
||||||
|
ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny},
|
||||||
MatchResources: &admissionregistration.MatchResources{
|
MatchResources: &admissionregistration.MatchResources{
|
||||||
MatchPolicy: func() *admissionregistration.MatchPolicyType {
|
MatchPolicy: func() *admissionregistration.MatchPolicyType {
|
||||||
r := admissionregistration.MatchPolicyType("Exact")
|
r := admissionregistration.MatchPolicyType("Exact")
|
||||||
@ -166,6 +168,7 @@ func newPolicyBinding(name string) *admissionregistration.ValidatingAdmissionPol
|
|||||||
ParamRef: &admissionregistration.ParamRef{
|
ParamRef: &admissionregistration.ParamRef{
|
||||||
Name: "param-test",
|
Name: "param-test",
|
||||||
},
|
},
|
||||||
|
ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny},
|
||||||
MatchResources: &admissionregistration.MatchResources{},
|
MatchResources: &admissionregistration.MatchResources{},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
|
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
|
||||||
|
|
||||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -59,6 +60,7 @@ func validPolicyBinding() *admissionregistration.ValidatingAdmissionPolicyBindin
|
|||||||
ParamRef: &admissionregistration.ParamRef{
|
ParamRef: &admissionregistration.ParamRef{
|
||||||
Name: "replica-limit-test.example.com",
|
Name: "replica-limit-test.example.com",
|
||||||
},
|
},
|
||||||
|
ValidationActions: []admissionregistration.ValidationAction{admissionregistration.Deny},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -107,18 +107,35 @@ type ValidatingAdmissionPolicySpec struct {
|
|||||||
MatchConstraints *MatchResources `json:"matchConstraints,omitempty" protobuf:"bytes,2,rep,name=matchConstraints"`
|
MatchConstraints *MatchResources `json:"matchConstraints,omitempty" protobuf:"bytes,2,rep,name=matchConstraints"`
|
||||||
|
|
||||||
// Validations contain CEL expressions which is used to apply the validation.
|
// Validations contain CEL expressions which is used to apply the validation.
|
||||||
// A minimum of one validation is required for a policy definition.
|
// Validations and AuditAnnotations may not both be empty; a minimum of one Validations or AuditAnnotations is
|
||||||
|
// required.
|
||||||
// +listType=atomic
|
// +listType=atomic
|
||||||
// Required.
|
// +optional
|
||||||
Validations []Validation `json:"validations" protobuf:"bytes,3,rep,name=validations"`
|
Validations []Validation `json:"validations,omitempty" protobuf:"bytes,3,rep,name=validations"`
|
||||||
|
|
||||||
// FailurePolicy defines how to handle failures for the admission policy.
|
// failurePolicy defines how to handle failures for the admission policy. Failures can
|
||||||
// Failures can occur from invalid or mis-configured policy definitions or bindings.
|
// occur from CEL expression parse errors, type check errors, runtime errors and invalid
|
||||||
|
// or mis-configured policy definitions or bindings.
|
||||||
|
//
|
||||||
// A policy is invalid if spec.paramKind refers to a non-existent Kind.
|
// A policy is invalid if spec.paramKind refers to a non-existent Kind.
|
||||||
// A binding is invalid if spec.paramRef.name refers to a non-existent resource.
|
// A binding is invalid if spec.paramRef.name refers to a non-existent resource.
|
||||||
|
//
|
||||||
|
// failurePolicy does not define how validations that evaluate to false are handled.
|
||||||
|
//
|
||||||
|
// When failurePolicy is set to Fail, ValidatingAdmissionPolicyBinding validationActions
|
||||||
|
// define how failures are enforced.
|
||||||
|
//
|
||||||
// Allowed values are Ignore or Fail. Defaults to Fail.
|
// Allowed values are Ignore or Fail. Defaults to Fail.
|
||||||
// +optional
|
// +optional
|
||||||
FailurePolicy *FailurePolicyType `json:"failurePolicy,omitempty" protobuf:"bytes,4,opt,name=failurePolicy,casttype=FailurePolicyType"`
|
FailurePolicy *FailurePolicyType `json:"failurePolicy,omitempty" protobuf:"bytes,4,opt,name=failurePolicy,casttype=FailurePolicyType"`
|
||||||
|
|
||||||
|
// auditAnnotations contains CEL expressions which are used to produce audit
|
||||||
|
// annotations for the audit event of the API request.
|
||||||
|
// validations and auditAnnotations may not both be empty; a least one of validations or auditAnnotations is
|
||||||
|
// required.
|
||||||
|
// +listType=atomic
|
||||||
|
// +optional
|
||||||
|
AuditAnnotations []AuditAnnotation `json:"auditAnnotations,omitempty" protobuf:"bytes,5,rep,name=auditAnnotations"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParamKind is a tuple of Group Kind and Version.
|
// ParamKind is a tuple of Group Kind and Version.
|
||||||
@ -138,11 +155,11 @@ type ParamKind struct {
|
|||||||
type Validation struct {
|
type Validation struct {
|
||||||
// Expression represents the expression which will be evaluated by CEL.
|
// Expression represents the expression which will be evaluated by CEL.
|
||||||
// ref: https://github.com/google/cel-spec
|
// ref: https://github.com/google/cel-spec
|
||||||
// CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables:
|
// CEL expressions have access to the contents of the API request/response, organized into CEL variables as well as some other useful variables:
|
||||||
//
|
//
|
||||||
// - 'object' - The object from the incoming request. The value is null for DELETE requests.
|
// - 'object' - The object from the incoming request. The value is null for DELETE requests.
|
||||||
// - 'oldObject' - The existing object. The value is null for CREATE requests.
|
// - 'oldObject' - The existing object. The value is null for CREATE requests.
|
||||||
// - 'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)).
|
// - 'request' - Attributes of the API request([ref](/pkg/apis/admission/types.go#AdmissionRequest)).
|
||||||
// - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.
|
// - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.
|
||||||
// - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.
|
// - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.
|
||||||
// See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz
|
// See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz
|
||||||
@ -194,6 +211,43 @@ type Validation struct {
|
|||||||
Reason *metav1.StatusReason `json:"reason,omitempty" protobuf:"bytes,3,opt,name=reason"`
|
Reason *metav1.StatusReason `json:"reason,omitempty" protobuf:"bytes,3,opt,name=reason"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuditAnnotation describes how to produce an audit annotation for an API request.
|
||||||
|
type AuditAnnotation struct {
|
||||||
|
// key specifies the audit annotation key. The audit annotation keys of
|
||||||
|
// a ValidatingAdmissionPolicy must be unique. The key must be a qualified
|
||||||
|
// name ([A-Za-z0-9][-A-Za-z0-9_.]*) no more than 63 bytes in length.
|
||||||
|
//
|
||||||
|
// The key is combined with the resource name of the
|
||||||
|
// ValidatingAdmissionPolicy to construct an audit annotation key:
|
||||||
|
// "{ValidatingAdmissionPolicy name}/{key}".
|
||||||
|
//
|
||||||
|
// If an admission webhook uses the same resource name as this ValidatingAdmissionPolicy
|
||||||
|
// and the same audit annotation key, the annotation key will be identical.
|
||||||
|
// In this case, the first annotation written with the key will be included
|
||||||
|
// in the audit event and all subsequent annotations with the same key
|
||||||
|
// will be discarded.
|
||||||
|
//
|
||||||
|
// Required.
|
||||||
|
Key string `json:"key" protobuf:"bytes,1,opt,name=key"`
|
||||||
|
|
||||||
|
// valueExpression represents the expression which is evaluated by CEL to
|
||||||
|
// produce an audit annotation value. The expression must evaluate to either
|
||||||
|
// a string or null value. If the expression evaluates to a string, the
|
||||||
|
// audit annotation is included with the string value. If the expression
|
||||||
|
// evaluates to null or empty string the audit annotation will be omitted.
|
||||||
|
// The valueExpression may be no longer than 5kb in length.
|
||||||
|
// If the result of the valueExpression is more than 10kb in length, it
|
||||||
|
// will be truncated to 10kb.
|
||||||
|
//
|
||||||
|
// If multiple ValidatingAdmissionPolicyBinding resources match an
|
||||||
|
// API request, then the valueExpression will be evaluated for
|
||||||
|
// each binding. All unique values produced by the valueExpressions
|
||||||
|
// will be joined together in a comma-separated list.
|
||||||
|
//
|
||||||
|
// Required.
|
||||||
|
ValueExpression string `json:"valueExpression" protobuf:"bytes,2,opt,name=valueExpression"`
|
||||||
|
}
|
||||||
|
|
||||||
// +genclient
|
// +genclient
|
||||||
// +genclient:nonNamespaced
|
// +genclient:nonNamespaced
|
||||||
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
|
||||||
@ -244,6 +298,48 @@ type ValidatingAdmissionPolicyBindingSpec struct {
|
|||||||
// Note that this is differs from ValidatingAdmissionPolicy matchConstraints, where resourceRules are required.
|
// Note that this is differs from ValidatingAdmissionPolicy matchConstraints, where resourceRules are required.
|
||||||
// +optional
|
// +optional
|
||||||
MatchResources *MatchResources `json:"matchResources,omitempty" protobuf:"bytes,3,rep,name=matchResources"`
|
MatchResources *MatchResources `json:"matchResources,omitempty" protobuf:"bytes,3,rep,name=matchResources"`
|
||||||
|
|
||||||
|
// validationActions declares how Validations of the referenced ValidatingAdmissionPolicy are enforced.
|
||||||
|
// If a validation evaluates to false it is always enforced according to these actions.
|
||||||
|
//
|
||||||
|
// Failures defined by the ValidatingAdmissionPolicy's FailurePolicy are enforced according
|
||||||
|
// to these actions only if the FailurePolicy is set to Fail, otherwise the failures are
|
||||||
|
// ignored. This includes compilation errors, runtime errors and misconfigurations of the policy.
|
||||||
|
//
|
||||||
|
// validationActions is declared as a set of action values. Order does
|
||||||
|
// not matter. validationActions may not contain duplicates of the same action.
|
||||||
|
//
|
||||||
|
// The supported actions values are:
|
||||||
|
//
|
||||||
|
// "Deny" specifies that a validation failure results in a denied request.
|
||||||
|
//
|
||||||
|
// "Warn" specifies that a validation failure is reported to the request client
|
||||||
|
// in HTTP Warning headers, with a warning code of 299. Warnings can be sent
|
||||||
|
// both for allowed or denied admission responses.
|
||||||
|
//
|
||||||
|
// "Audit" specifies that a validation failure is included in the published
|
||||||
|
// audit event for the request. The audit event will contain a
|
||||||
|
// `validation.policy.admission.k8s.io/validation_failure` audit annotation
|
||||||
|
// with a value containing the details of the validation failures, formatted as
|
||||||
|
// a JSON list of objects, each with the following fields:
|
||||||
|
// - message: The validation failure message string
|
||||||
|
// - policy: The resource name of the ValidatingAdmissionPolicy
|
||||||
|
// - binding: The resource name of the ValidatingAdmissionPolicyBinding
|
||||||
|
// - expressionIndex: The index of the failed validations in the ValidatingAdmissionPolicy
|
||||||
|
// - validationActions: The enforcement actions enacted for the validation failure
|
||||||
|
// Example audit annotation:
|
||||||
|
// `"validation.policy.admission.k8s.io/validation_failure": "[{\"message\": \"Invalid value\", {\"policy\": \"policy.example.com\", {\"binding\": \"policybinding.example.com\", {\"expressionIndex\": \"1\", {\"validationActions\": [\"Audit\"]}]"`
|
||||||
|
//
|
||||||
|
// Clients should expect to handle additional values by ignoring
|
||||||
|
// any values not recognized.
|
||||||
|
//
|
||||||
|
// "Deny" and "Warn" may not be used together since this combination
|
||||||
|
// needlessly duplicates the validation failure both in the
|
||||||
|
// API response body and the HTTP warning headers.
|
||||||
|
//
|
||||||
|
// Required.
|
||||||
|
// +listType=set
|
||||||
|
ValidationActions []ValidationAction `json:"validationActions,omitempty" protobuf:"bytes,4,rep,name=validationActions"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParamRef references a parameter resource
|
// ParamRef references a parameter resource
|
||||||
@ -348,6 +444,24 @@ type MatchResources struct {
|
|||||||
MatchPolicy *MatchPolicyType `json:"matchPolicy,omitempty" protobuf:"bytes,7,opt,name=matchPolicy,casttype=MatchPolicyType"`
|
MatchPolicy *MatchPolicyType `json:"matchPolicy,omitempty" protobuf:"bytes,7,opt,name=matchPolicy,casttype=MatchPolicyType"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ValidationAction specifies a policy enforcement action.
|
||||||
|
// +enum
|
||||||
|
type ValidationAction string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Deny specifies that a validation failure results in a denied request.
|
||||||
|
Deny ValidationAction = "Deny"
|
||||||
|
// Warn specifies that a validation failure is reported to the request client
|
||||||
|
// in HTTP Warning headers, with a warning code of 299. Warnings can be sent
|
||||||
|
// both for allowed or denied admission responses.
|
||||||
|
Warn ValidationAction = "Warn"
|
||||||
|
// Audit specifies that a validation failure is included in the published
|
||||||
|
// audit event for the request. The audit event will contain a
|
||||||
|
// `validation.policy.admission.k8s.io/validation_failure` audit annotation
|
||||||
|
// with a value containing the details of the validation failure.
|
||||||
|
Audit ValidationAction = "Audit"
|
||||||
|
)
|
||||||
|
|
||||||
// NamedRuleWithOperations is a tuple of Operations and Resources with ResourceNames.
|
// NamedRuleWithOperations is a tuple of Operations and Resources with ResourceNames.
|
||||||
// +structType=atomic
|
// +structType=atomic
|
||||||
type NamedRuleWithOperations struct {
|
type NamedRuleWithOperations struct {
|
||||||
|
@ -109,3 +109,15 @@ func (m *ValidatingAdmissionPolicyMetrics) ObserveRejection(ctx context.Context,
|
|||||||
m.policyCheck.WithContext(ctx).WithLabelValues(policy, binding, "deny", state).Inc()
|
m.policyCheck.WithContext(ctx).WithLabelValues(policy, binding, "deny", state).Inc()
|
||||||
m.policyLatency.WithContext(ctx).WithLabelValues(policy, binding, "deny", state).Observe(elapsed.Seconds())
|
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())
|
||||||
|
}
|
||||||
|
@ -223,11 +223,26 @@ func CompileCELExpression(expressionAccessor ExpressionAccessor, optionalVars Op
|
|||||||
ExpressionAccessor: expressionAccessor,
|
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{
|
return CompilationResult{
|
||||||
Error: &apiservercel.Error{
|
Error: &apiservercel.Error{
|
||||||
Type: apiservercel.ErrorTypeInvalid,
|
Type: apiservercel.ErrorTypeInvalid,
|
||||||
Detail: "cel expression must evaluate to a bool",
|
Detail: reason,
|
||||||
},
|
},
|
||||||
ExpressionAccessor: expressionAccessor,
|
ExpressionAccessor: expressionAccessor,
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,8 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
celgo "github.com/google/cel-go/cel"
|
||||||
|
|
||||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -120,34 +122,79 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
|||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
for _, expr := range tc.expressions {
|
for _, expr := range tc.expressions {
|
||||||
result := CompileCELExpression(&fakeExpressionAccessor{
|
t.Run(expr, func(t *testing.T) {
|
||||||
expr,
|
t.Run("expression", func(t *testing.T) {
|
||||||
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: true}, celconfig.PerCallLimit)
|
result := CompileCELExpression(&fakeValidationCondition{
|
||||||
|
Expression: expr,
|
||||||
|
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit)
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
t.Errorf("Unexpected error: %v", result.Error)
|
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 {
|
for expr, expectErr := range tc.errorExpressions {
|
||||||
result := CompileCELExpression(&fakeExpressionAccessor{
|
t.Run(expr, func(t *testing.T) {
|
||||||
expr,
|
t.Run("expression", func(t *testing.T) {
|
||||||
|
result := CompileCELExpression(&fakeValidationCondition{
|
||||||
|
Expression: expr,
|
||||||
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit)
|
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer}, celconfig.PerCallLimit)
|
||||||
if result.Error == nil {
|
if result.Error == nil {
|
||||||
t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
|
t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
|
||||||
continue
|
return
|
||||||
}
|
}
|
||||||
if !strings.Contains(result.Error.Error(), expectErr) {
|
if !strings.Contains(result.Error.Error(), expectErr) {
|
||||||
t.Errorf("Expected compilation '%s' error to contain '%v' but got: %v", expr, expectErr, result.Error)
|
t.Errorf("Expected validation '%s' error to contain '%v' but got: %v", expr, expectErr, result.Error)
|
||||||
}
|
}
|
||||||
continue
|
})
|
||||||
|
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 {
|
type fakeValidationCondition struct {
|
||||||
expression string
|
Expression string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeExpressionAccessor) GetExpression() string {
|
func (v *fakeValidationCondition) GetExpression() string {
|
||||||
return f.expression
|
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}
|
||||||
}
|
}
|
||||||
|
@ -75,11 +75,7 @@ func (a *evaluationActivation) Parent() interpreter.Activation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compile compiles the cel expressions defined in the ExpressionAccessors into a Filter
|
// 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 {
|
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations, perCallLimit uint64) Filter {
|
||||||
if len(expressionAccessors) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
compilationResults := make([]CompilationResult, len(expressionAccessors))
|
compilationResults := make([]CompilationResult, len(expressionAccessors))
|
||||||
for i, expressionAccessor := range expressionAccessors {
|
for i, expressionAccessor := range expressionAccessors {
|
||||||
compilationResults[i] = CompileCELExpression(expressionAccessor, options, perCallLimit)
|
compilationResults[i] = CompileCELExpression(expressionAccessor, options, perCallLimit)
|
||||||
|
@ -24,14 +24,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
celgo "github.com/google/cel-go/cel"
|
||||||
celtypes "github.com/google/cel-go/common/types"
|
celtypes "github.com/google/cel-go/common/types"
|
||||||
"github.com/stretchr/testify/require"
|
"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"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
@ -39,6 +35,10 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
"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 {
|
type condition struct {
|
||||||
@ -49,6 +49,10 @@ func (c *condition) GetExpression() string {
|
|||||||
return c.Expression
|
return c.Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *condition) ReturnTypes() []*celgo.Type {
|
||||||
|
return []*celgo.Type{celgo.BoolType}
|
||||||
|
}
|
||||||
|
|
||||||
func TestCompile(t *testing.T) {
|
func TestCompile(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
|
@ -19,6 +19,7 @@ package cel
|
|||||||
import (
|
import (
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/google/cel-go/cel"
|
||||||
"github.com/google/cel-go/common/types/ref"
|
"github.com/google/cel-go/common/types/ref"
|
||||||
|
|
||||||
v1 "k8s.io/api/admission/v1"
|
v1 "k8s.io/api/admission/v1"
|
||||||
@ -31,6 +32,7 @@ var _ ExpressionAccessor = &MatchCondition{}
|
|||||||
|
|
||||||
type ExpressionAccessor interface {
|
type ExpressionAccessor interface {
|
||||||
GetExpression() string
|
GetExpression() string
|
||||||
|
ReturnTypes() []*cel.Type
|
||||||
}
|
}
|
||||||
|
|
||||||
// EvaluationResult contains the minimal required fields and metadata of a cel evaluation
|
// EvaluationResult contains the minimal required fields and metadata of a cel evaluation
|
||||||
@ -50,6 +52,10 @@ func (v *MatchCondition) GetExpression() string {
|
|||||||
return v.Expression
|
return v.Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (v *MatchCondition) ReturnTypes() []*cel.Type {
|
||||||
|
return []*cel.Type{cel.BoolType}
|
||||||
|
}
|
||||||
|
|
||||||
// OptionalVariableDeclarations declares which optional CEL variables
|
// OptionalVariableDeclarations declares which optional CEL variables
|
||||||
// are declared for an expression.
|
// are declared for an expression.
|
||||||
type OptionalVariableDeclarations struct {
|
type OptionalVariableDeclarations struct {
|
||||||
@ -83,6 +89,7 @@ type OptionalVariableBindings struct {
|
|||||||
// Filter contains a function to evaluate compiled CEL-typed values
|
// Filter contains a function to evaluate compiled CEL-typed values
|
||||||
// It expects the inbound object to already have been converted to the version expected
|
// 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).
|
// by the underlying CEL code (which is indicated by the match criteria of a policy definition).
|
||||||
|
// versionedParams may be nil.
|
||||||
type Filter interface {
|
type Filter interface {
|
||||||
// ForInput converts compiled CEL-typed values into evaluated CEL-typed values
|
// 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.
|
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
|
||||||
|
@ -19,11 +19,13 @@ package validatingadmissionpolicy
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
celgo "github.com/google/cel-go/cel"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
@ -38,14 +40,18 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"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/apimachinery/pkg/util/wait"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/admission/initializer"
|
"k8s.io/apiserver/pkg/admission/initializer"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
|
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
|
||||||
whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/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/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/features"
|
"k8s.io/apiserver/pkg/features"
|
||||||
|
"k8s.io/apiserver/pkg/warning"
|
||||||
dynamicfake "k8s.io/client-go/dynamic/fake"
|
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
"k8s.io/client-go/kubernetes/fake"
|
"k8s.io/client-go/kubernetes/fake"
|
||||||
@ -144,6 +150,7 @@ var (
|
|||||||
Name: fakeParams.GetName(),
|
Name: fakeParams.GetName(),
|
||||||
Namespace: fakeParams.GetNamespace(),
|
Namespace: fakeParams.GetNamespace(),
|
||||||
},
|
},
|
||||||
|
ValidationActions: []v1alpha1.ValidationAction{v1alpha1.Deny},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
denyBindingWithNoParamRef *v1alpha1.ValidatingAdmissionPolicyBinding = &v1alpha1.ValidatingAdmissionPolicyBinding{
|
denyBindingWithNoParamRef *v1alpha1.ValidatingAdmissionPolicyBinding = &v1alpha1.ValidatingAdmissionPolicyBinding{
|
||||||
@ -153,6 +160,38 @@ var (
|
|||||||
},
|
},
|
||||||
Spec: v1alpha1.ValidatingAdmissionPolicyBindingSpec{
|
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,
|
options cel.OptionalVariableDeclarations,
|
||||||
perCallLimit uint64,
|
perCallLimit uint64,
|
||||||
) cel.Filter {
|
) cel.Filter {
|
||||||
|
if len(expressions) > 0 {
|
||||||
key := expressions[0].GetExpression()
|
key := expressions[0].GetExpression()
|
||||||
if fun, ok := f.CompileFuncs[key]; ok {
|
if fun, ok := f.CompileFuncs[key]; ok {
|
||||||
return fun(expressions, options)
|
return fun(expressions, options)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return &fakeFilter{}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeCompiler) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, compileFunc func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter) {
|
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 ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (f *fakeEvalRequest) ReturnTypes() []*celgo.Type {
|
||||||
|
return []*celgo.Type{celgo.BoolType}
|
||||||
|
}
|
||||||
|
|
||||||
var _ cel.Filter = &fakeFilter{}
|
var _ cel.Filter = &fakeFilter{}
|
||||||
|
|
||||||
type fakeFilter struct {
|
type fakeFilter struct {
|
||||||
@ -220,22 +264,28 @@ func (f *fakeFilter) CompilationErrors() []error {
|
|||||||
var _ Validator = &fakeValidator{}
|
var _ Validator = &fakeValidator{}
|
||||||
|
|
||||||
type fakeValidator struct {
|
type fakeValidator struct {
|
||||||
*fakeFilter
|
validationFilter, auditAnnotationFilter *fakeFilter
|
||||||
ValidateFunc func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision
|
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
|
//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 {
|
if validatorMap == nil {
|
||||||
validatorMap = make(map[string]*fakeValidator)
|
validatorMap = make(map[string]*fakeValidator)
|
||||||
}
|
}
|
||||||
|
|
||||||
f.ValidateFunc = validateFunc
|
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)
|
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
|
// Override compiler used by controller for tests
|
||||||
controller = handler.evaluator.(*celAdmissionController)
|
controller = handler.evaluator.(*celAdmissionController)
|
||||||
controller.policyController.filterCompiler = compiler
|
controller.policyController.filterCompiler = compiler
|
||||||
controller.policyController.newValidator = func(filter cel.Filter, fail *admissionRegistrationv1.FailurePolicyType, authorizer authorizer.Authorizer) Validator {
|
controller.policyController.newValidator = func(validationFilter, auditAnnotationFilter cel.Filter, fail *admissionRegistrationv1.FailurePolicyType, authorizer authorizer.Authorizer) Validator {
|
||||||
f := filter.(*fakeFilter)
|
f := validationFilter.(*fakeFilter)
|
||||||
v := validatorMap[f.keyId]
|
v := validatorMap[f.keyId]
|
||||||
v.fakeFilter = f
|
v.validationFilter = f
|
||||||
|
v.auditAnnotationFilter = auditAnnotationFilter.(*fakeFilter)
|
||||||
return v
|
return v
|
||||||
}
|
}
|
||||||
controller.policyController.matcher = matcher
|
controller.policyController.matcher = matcher
|
||||||
@ -596,7 +647,7 @@ func waitForReconcileDeletion(ctx context.Context, controller *celAdmissionContr
|
|||||||
func attributeRecord(
|
func attributeRecord(
|
||||||
old, new runtime.Object,
|
old, new runtime.Object,
|
||||||
operation admission.Operation,
|
operation admission.Operation,
|
||||||
) admission.Attributes {
|
) *FakeAttributes {
|
||||||
if old == nil && new == nil {
|
if old == nil && new == nil {
|
||||||
panic("both `old` and `new` may not be nil")
|
panic("both `old` and `new` may not be nil")
|
||||||
}
|
}
|
||||||
@ -622,7 +673,8 @@ func attributeRecord(
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return admission.NewAttributesRecord(
|
return &FakeAttributes{
|
||||||
|
Attributes: admission.NewAttributesRecord(
|
||||||
new,
|
new,
|
||||||
old,
|
old,
|
||||||
gvk,
|
gvk,
|
||||||
@ -634,7 +686,8 @@ func attributeRecord(
|
|||||||
nil,
|
nil,
|
||||||
false,
|
false,
|
||||||
nil,
|
nil,
|
||||||
)
|
),
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func ptrTo[T any](obj T) *T {
|
func ptrTo[T any](obj T) *T {
|
||||||
@ -716,12 +769,14 @@ func TestBasicPolicyDefinitionFailure(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
|
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
|
||||||
return []PolicyDecision{
|
return ValidateResult{
|
||||||
|
Decisions: []PolicyDecision{
|
||||||
{
|
{
|
||||||
Action: ActionDeny,
|
Action: ActionDeny,
|
||||||
Message: "Denied",
|
Message: "Denied",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -737,14 +792,22 @@ func TestBasicPolicyDefinitionFailure(t *testing.T) {
|
|||||||
testContext, controller,
|
testContext, controller,
|
||||||
fakeParams, denyBinding, denyPolicy))
|
fakeParams, denyBinding, denyPolicy))
|
||||||
|
|
||||||
|
warningRecorder := newWarningRecorder()
|
||||||
|
warnCtx := warning.WithWarningRecorder(testContext, warningRecorder)
|
||||||
|
attr := attributeRecord(nil, fakeParams, admission.Create)
|
||||||
err := handler.Validate(
|
err := handler.Validate(
|
||||||
testContext,
|
warnCtx,
|
||||||
// Object is irrelevant/unchecked for this test. Just test that
|
// Object is irrelevant/unchecked for this test. Just test that
|
||||||
// the evaluator is executed, and returns a denial
|
// the evaluator is executed, and returns a denial
|
||||||
attributeRecord(nil, fakeParams, admission.Create),
|
attr,
|
||||||
&admission.RuntimeObjectInterfaces{},
|
&admission.RuntimeObjectInterfaces{},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
require.Equal(t, 0, warningRecorder.len())
|
||||||
|
|
||||||
|
annotations := attr.GetAnnotations(auditinternal.LevelMetadata)
|
||||||
|
require.Equal(t, 0, len(annotations))
|
||||||
|
|
||||||
require.ErrorContains(t, err, `Denied`)
|
require.ErrorContains(t, err, `Denied`)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -776,12 +839,14 @@ func TestDefinitionDoesntMatch(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
|
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
|
||||||
return []PolicyDecision{
|
return ValidateResult{
|
||||||
|
Decisions: []PolicyDecision{
|
||||||
{
|
{
|
||||||
Action: ActionDeny,
|
Action: ActionDeny,
|
||||||
Message: "Denied",
|
Message: "Denied",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -887,12 +952,14 @@ func TestReconfigureBinding(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
|
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
|
||||||
return []PolicyDecision{
|
return ValidateResult{
|
||||||
|
Decisions: []PolicyDecision{
|
||||||
{
|
{
|
||||||
Action: ActionDeny,
|
Action: ActionDeny,
|
||||||
Message: "Denied",
|
Message: "Denied",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -907,6 +974,7 @@ func TestReconfigureBinding(t *testing.T) {
|
|||||||
Name: fakeParams2.GetName(),
|
Name: fakeParams2.GetName(),
|
||||||
Namespace: fakeParams2.GetNamespace(),
|
Namespace: fakeParams2.GetNamespace(),
|
||||||
},
|
},
|
||||||
|
ValidationActions: []v1alpha1.ValidationAction{v1alpha1.Deny},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -994,12 +1062,14 @@ func TestRemoveDefinition(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
|
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
|
||||||
return []PolicyDecision{
|
return ValidateResult{
|
||||||
|
Decisions: []PolicyDecision{
|
||||||
{
|
{
|
||||||
Action: ActionDeny,
|
Action: ActionDeny,
|
||||||
Message: "Denied",
|
Message: "Denied",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1061,12 +1131,14 @@ func TestRemoveBinding(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
|
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
|
||||||
return []PolicyDecision{
|
return ValidateResult{
|
||||||
|
Decisions: []PolicyDecision{
|
||||||
{
|
{
|
||||||
Action: ActionDeny,
|
Action: ActionDeny,
|
||||||
Message: "Denied",
|
Message: "Denied",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1169,12 +1241,14 @@ func TestInvalidParamSourceInstanceName(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
|
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
|
||||||
return []PolicyDecision{
|
return ValidateResult{
|
||||||
|
Decisions: []PolicyDecision{
|
||||||
{
|
{
|
||||||
Action: ActionDeny,
|
Action: ActionDeny,
|
||||||
Message: "Denied",
|
Message: "Denied",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1235,12 +1309,14 @@ func TestEmptyParamSource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) []PolicyDecision {
|
validator.RegisterDefinition(denyPolicy, func(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64) ValidateResult {
|
||||||
return []PolicyDecision{
|
return ValidateResult{
|
||||||
|
Decisions: []PolicyDecision{
|
||||||
{
|
{
|
||||||
Action: ActionDeny,
|
Action: ActionDeny,
|
||||||
Message: "Denied",
|
Message: "Denied",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1335,12 +1411,14 @@ 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)
|
evaluations1.Add(1)
|
||||||
return []PolicyDecision{
|
return ValidateResult{
|
||||||
|
Decisions: []PolicyDecision{
|
||||||
{
|
{
|
||||||
Action: ActionAdmit,
|
Action: ActionAdmit,
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1352,13 +1430,15 @@ 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)
|
evaluations2.Add(1)
|
||||||
return []PolicyDecision{
|
return ValidateResult{
|
||||||
|
Decisions: []PolicyDecision{
|
||||||
{
|
{
|
||||||
Action: ActionDeny,
|
Action: ActionDeny,
|
||||||
Message: "Policy2Denied",
|
Message: "Policy2Denied",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1460,21 +1540,25 @@ 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)
|
evaluations.Add(1)
|
||||||
if _, ok := versionedParams.(*v1.ConfigMap); ok {
|
if _, ok := versionedParams.(*v1.ConfigMap); ok {
|
||||||
return []PolicyDecision{
|
return ValidateResult{
|
||||||
|
Decisions: []PolicyDecision{
|
||||||
{
|
{
|
||||||
Action: ActionDeny,
|
Action: ActionDeny,
|
||||||
Message: "correct type",
|
Message: "correct type",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return []PolicyDecision{
|
return ValidateResult{
|
||||||
|
Decisions: []PolicyDecision{
|
||||||
{
|
{
|
||||||
Action: ActionDeny,
|
Action: ActionDeny,
|
||||||
Message: "Incorrect param type",
|
Message: "Incorrect param type",
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -1516,6 +1600,366 @@ func TestNativeTypeParam(t *testing.T) {
|
|||||||
require.EqualValues(t, 1, evaluations.Load())
|
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{}
|
type fakeAuthorizer struct{}
|
||||||
|
|
||||||
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||||
|
@ -20,16 +20,19 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
"k8s.io/api/admissionregistration/v1alpha1"
|
"k8s.io/api/admissionregistration/v1alpha1"
|
||||||
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
k8serrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
|
utiljson "k8s.io/apimachinery/pkg/util/json"
|
||||||
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||||
"k8s.io/apimachinery/pkg/util/sets"
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
"k8s.io/apimachinery/pkg/util/wait"
|
"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/internal/generic"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching"
|
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching"
|
||||||
whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
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/authorization/authorizer"
|
||||||
|
"k8s.io/apiserver/pkg/warning"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
@ -169,6 +174,8 @@ func (c *celAdmissionController) Run(stopCh <-chan struct{}) {
|
|||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const maxAuditAnnotationValueLength = 10 * 1024
|
||||||
|
|
||||||
func (c *celAdmissionController) Validate(
|
func (c *celAdmissionController) Validate(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
a admission.Attributes,
|
a admission.Attributes,
|
||||||
@ -239,6 +246,7 @@ func (c *celAdmissionController) Validate(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
auditAnnotationCollector := newAuditAnnotationCollector()
|
||||||
for _, bindingInfo := range definitionInfo.bindings {
|
for _, bindingInfo := range definitionInfo.bindings {
|
||||||
// If the key is inside dependentBindings, there is guaranteed to
|
// If the key is inside dependentBindings, there is guaranteed to
|
||||||
// be a bindingInfo for it
|
// be a bindingInfo for it
|
||||||
@ -321,27 +329,72 @@ func (c *celAdmissionController) Validate(
|
|||||||
versionedAttr = va
|
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 {
|
switch decision.Action {
|
||||||
case ActionAdmit:
|
case ActionAdmit:
|
||||||
if decision.Evaluation == EvalError {
|
if decision.Evaluation == EvalError {
|
||||||
celmetrics.Metrics.ObserveAdmissionWithError(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
|
celmetrics.Metrics.ObserveAdmissionWithError(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
|
||||||
}
|
}
|
||||||
case ActionDeny:
|
case ActionDeny:
|
||||||
|
for _, action := range binding.Spec.ValidationActions {
|
||||||
|
switch action {
|
||||||
|
case v1alpha1.Deny:
|
||||||
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
|
deniedDecisions = append(deniedDecisions, policyDecisionWithMetadata{
|
||||||
Definition: definition,
|
Definition: definition,
|
||||||
Binding: binding,
|
Binding: binding,
|
||||||
PolicyDecision: decision,
|
PolicyDecision: decision,
|
||||||
})
|
})
|
||||||
celmetrics.Metrics.ObserveRejection(ctx, decision.Elapsed, definition.Name, binding.Name, "active")
|
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:
|
default:
|
||||||
return fmt.Errorf("unrecognized evaluation decision '%s' for ValidatingAdmissionPolicyBinding '%s' with ValidatingAdmissionPolicy '%s'",
|
return fmt.Errorf("unrecognized evaluation decision '%s' for ValidatingAdmissionPolicyBinding '%s' with ValidatingAdmissionPolicy '%s'",
|
||||||
decision.Action, binding.Name, definition.Name)
|
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 {
|
if len(deniedDecisions) > 0 {
|
||||||
@ -366,6 +419,25 @@ func (c *celAdmissionController) Validate(
|
|||||||
return nil
|
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 {
|
func (c *celAdmissionController) HasSynced() bool {
|
||||||
return c.policyController.HasSynced() && c.definitions.Load() != nil
|
return c.policyController.HasSynced() && c.definitions.Load() != nil
|
||||||
}
|
}
|
||||||
@ -377,3 +449,48 @@ func (c *celAdmissionController) ValidateInitialization() error {
|
|||||||
func (c *celAdmissionController) refreshPolicies() {
|
func (c *celAdmissionController) refreshPolicies() {
|
||||||
c.definitions.Store(c.policyController.latestPolicyData())
|
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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -92,7 +92,7 @@ type policyController struct {
|
|||||||
authz authorizer.Authorizer
|
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(
|
func newPolicyController(
|
||||||
restMapper meta.RESTMapper,
|
restMapper meta.RESTMapper,
|
||||||
@ -461,6 +461,7 @@ func (c *policyController) latestPolicyData() []policyData {
|
|||||||
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
|
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
|
||||||
bindingInfo.validator = c.newValidator(
|
bindingInfo.validator = c.newValidator(
|
||||||
c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars, celconfig.PerCallLimit),
|
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),
|
convertv1alpha1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy),
|
||||||
c.authz,
|
c.authz,
|
||||||
)
|
)
|
||||||
@ -513,6 +514,18 @@ func convertv1alpha1Validations(inputValidations []v1alpha1.Validation) []cel.Ex
|
|||||||
return celExpressionAccessor
|
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 {
|
func getNamespaceName(namespace, name string) namespacedName {
|
||||||
return namespacedName{
|
return namespacedName{
|
||||||
namespace: namespace,
|
namespace: namespace,
|
||||||
|
@ -17,6 +17,8 @@ limitations under the License.
|
|||||||
package validatingadmissionpolicy
|
package validatingadmissionpolicy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
celgo "github.com/google/cel-go/cel"
|
||||||
|
|
||||||
"k8s.io/api/admissionregistration/v1alpha1"
|
"k8s.io/api/admissionregistration/v1alpha1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
@ -39,6 +41,24 @@ func (v *ValidationCondition) GetExpression() string {
|
|||||||
return v.Expression
|
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
|
// Matcher is used for matching ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding to attributes
|
||||||
type Matcher interface {
|
type Matcher interface {
|
||||||
admission.InitializationValidator
|
admission.InitializationValidator
|
||||||
@ -52,9 +72,17 @@ type Matcher interface {
|
|||||||
BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error)
|
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
|
// Validator is contains logic for converting ValidationEvaluation to PolicyDecisions
|
||||||
type Validator interface {
|
type Validator interface {
|
||||||
// Validate is used to take cel evaluations and convert into decisions
|
// 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.
|
// 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
|
||||||
}
|
}
|
||||||
|
@ -47,6 +47,29 @@ type PolicyDecision struct {
|
|||||||
Elapsed time.Duration
|
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 {
|
func reasonToCode(r metav1.StatusReason) int32 {
|
||||||
switch r {
|
switch r {
|
||||||
case metav1.StatusReasonForbidden:
|
case metav1.StatusReasonForbidden:
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
celtypes "github.com/google/cel-go/common/types"
|
celtypes "github.com/google/cel-go/common/types"
|
||||||
|
|
||||||
"k8s.io/klog/v2"
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
v1 "k8s.io/api/admissionregistration/v1"
|
v1 "k8s.io/api/admissionregistration/v1"
|
||||||
@ -33,14 +34,16 @@ import (
|
|||||||
|
|
||||||
// validator implements the Validator interface
|
// validator implements the Validator interface
|
||||||
type validator struct {
|
type validator struct {
|
||||||
filter cel.Filter
|
validationFilter cel.Filter
|
||||||
|
auditAnnotationFilter cel.Filter
|
||||||
failPolicy *v1.FailurePolicyType
|
failPolicy *v1.FailurePolicyType
|
||||||
authorizer authorizer.Authorizer
|
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{
|
return &validator{
|
||||||
filter: filter,
|
validationFilter: validationFilter,
|
||||||
|
auditAnnotationFilter: auditAnnotationFilter,
|
||||||
failPolicy: failPolicy,
|
failPolicy: failPolicy,
|
||||||
authorizer: authorizer,
|
authorizer: authorizer,
|
||||||
}
|
}
|
||||||
@ -53,9 +56,16 @@ func policyDecisionActionForError(f v1.FailurePolicyType) PolicyDecisionAction {
|
|||||||
return ActionDeny
|
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
|
// 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.
|
// 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
|
var f v1.FailurePolicyType
|
||||||
if v.failPolicy == nil {
|
if v.failPolicy == nil {
|
||||||
f = v1.Fail
|
f = v1.Fail
|
||||||
@ -64,14 +74,16 @@ func (v *validator) Validate(versionedAttr *generic.VersionedAttributes, version
|
|||||||
}
|
}
|
||||||
|
|
||||||
optionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams, Authorizer: v.authorizer}
|
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 {
|
if err != nil {
|
||||||
return []PolicyDecision{
|
return ValidateResult{
|
||||||
|
Decisions: []PolicyDecision{
|
||||||
{
|
{
|
||||||
Action: policyDecisionActionForError(f),
|
Action: policyDecisionActionForError(f),
|
||||||
Evaluation: EvalError,
|
Evaluation: EvalError,
|
||||||
Message: err.Error(),
|
Message: err.Error(),
|
||||||
},
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
decisions := make([]PolicyDecision, len(evalResults))
|
decisions := make([]PolicyDecision, len(evalResults))
|
||||||
@ -104,11 +116,60 @@ func (v *validator) Validate(versionedAttr *generic.VersionedAttributes, version
|
|||||||
} else {
|
} else {
|
||||||
decision.Message = fmt.Sprintf("failed expression: %v", strings.TrimSpace(validation.Expression))
|
decision.Message = fmt.Sprintf("failed expression: %v", strings.TrimSpace(validation.Expression))
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
decision.Action = ActionAdmit
|
decision.Action = ActionAdmit
|
||||||
decision.Evaluation = EvalAdmit
|
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}
|
||||||
}
|
}
|
||||||
|
@ -18,6 +18,7 @@ package validatingadmissionpolicy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -67,7 +68,9 @@ func TestValidate(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
failPolicy *v1.FailurePolicyType
|
failPolicy *v1.FailurePolicyType
|
||||||
evaluations []cel.EvaluationResult
|
evaluations []cel.EvaluationResult
|
||||||
|
auditEvaluations []cel.EvaluationResult
|
||||||
policyDecision []PolicyDecision
|
policyDecision []PolicyDecision
|
||||||
|
auditAnnotations []PolicyAuditAnnotation
|
||||||
throwError bool
|
throwError bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@ -455,30 +458,161 @@ func TestValidate(t *testing.T) {
|
|||||||
failPolicy: &fail,
|
failPolicy: &fail,
|
||||||
throwError: true,
|
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 {
|
for _, tc := range cases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
v := validator{
|
v := validator{
|
||||||
failPolicy: tc.failPolicy,
|
failPolicy: tc.failPolicy,
|
||||||
filter: &fakeCelFilter{
|
validationFilter: &fakeCelFilter{
|
||||||
evaluations: tc.evaluations,
|
evaluations: tc.evaluations,
|
||||||
throwError: tc.throwError,
|
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(validateResult.Decisions), len(tc.policyDecision))
|
||||||
|
|
||||||
require.Equal(t, len(policyResults), len(tc.policyDecision))
|
|
||||||
|
|
||||||
for i, policyDecision := range tc.policyDecision {
|
for i, policyDecision := range tc.policyDecision {
|
||||||
if policyDecision.Action != policyResults[i].Action {
|
if policyDecision.Action != validateResult.Decisions[i].Action {
|
||||||
t.Errorf("Expected policy decision kind '%v' but got '%v'", policyDecision.Action, policyResults[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) {
|
if !strings.Contains(validateResult.Decisions[i].Message, policyDecision.Message) {
|
||||||
t.Errorf("Expected policy decision message contains '%v' but got '%v'", policyDecision.Message, policyResults[i].Message)
|
t.Errorf("Expected policy decision message contains '%v' but got '%v'", policyDecision.Message, validateResult.Decisions[i].Message)
|
||||||
}
|
}
|
||||||
if policyDecision.Reason != policyResults[i].Reason {
|
if policyDecision.Reason != validateResult.Decisions[i].Reason {
|
||||||
t.Errorf("Expected policy decision reason '%v' but got '%v'", policyDecision.Reason, policyResults[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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user