Add MutatingAdmissionPolicy API

This is closely aligned with ValidatingAdmissionPolicy
except that instead of validations that can fail with
messages, there are mutations, which can be defined
either with as an ApplyConfiguration or JSONPatch.

Co-authored-by: cici37 <cicih@google.com>
This commit is contained in:
Joe Betz 2024-10-25 13:25:46 -04:00
parent 4b13362dda
commit 3a1733f302
34 changed files with 4911 additions and 12 deletions

View File

@ -143,6 +143,10 @@ func TestDefaulting(t *testing.T) {
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyList"}: {},
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyBinding"}: {},
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "ValidatingAdmissionPolicyBindingList"}: {},
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "MutatingAdmissionPolicy"}: {},
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "MutatingAdmissionPolicyList"}: {},
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "MutatingAdmissionPolicyBinding"}: {},
{Group: "admissionregistration.k8s.io", Version: "v1alpha1", Kind: "MutatingAdmissionPolicyBindingList"}: {},
{Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfiguration"}: {},
{Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "ValidatingWebhookConfigurationList"}: {},
{Group: "admissionregistration.k8s.io", Version: "v1beta1", Kind: "MutatingWebhookConfiguration"}: {},

View File

@ -107,5 +107,28 @@ var Funcs = func(codecs runtimeserializer.CodecFactory) []interface{} {
obj.ParameterNotFoundAction = &v
}
},
func(obj *admissionregistration.MutatingAdmissionPolicySpec, c fuzz.Continue) {
c.FuzzNoCustom(obj) // fuzz self without calling this function again
if obj.FailurePolicy == nil {
p := admissionregistration.FailurePolicyType("Fail")
obj.FailurePolicy = &p
}
obj.ReinvocationPolicy = admissionregistration.NeverReinvocationPolicy
},
func(obj *admissionregistration.Mutation, c fuzz.Continue) {
c.FuzzNoCustom(obj) // fuzz self without calling this function again
patchTypes := []admissionregistration.PatchType{admissionregistration.PatchTypeJSONPatch, admissionregistration.PatchTypeApplyConfiguration}
obj.PatchType = patchTypes[c.Rand.Intn(len(patchTypes))]
if obj.PatchType == admissionregistration.PatchTypeJSONPatch {
obj.JSONPatch = &admissionregistration.JSONPatch{}
c.Fuzz(&obj.JSONPatch)
obj.ApplyConfiguration = nil
}
if obj.PatchType == admissionregistration.PatchTypeApplyConfiguration {
obj.ApplyConfiguration = &admissionregistration.ApplyConfiguration{}
c.Fuzz(obj.ApplyConfiguration)
obj.JSONPatch = nil
}
},
}
}

View File

@ -55,6 +55,10 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&ValidatingAdmissionPolicyList{},
&ValidatingAdmissionPolicyBinding{},
&ValidatingAdmissionPolicyBindingList{},
&MutatingAdmissionPolicy{},
&MutatingAdmissionPolicyList{},
&MutatingAdmissionPolicyBinding{},
&MutatingAdmissionPolicyBindingList{},
)
return nil
}

View File

@ -206,7 +206,7 @@ type ValidatingAdmissionPolicySpec struct {
ParamKind *ParamKind
// MatchConstraints specifies what resources this policy is designed to validate.
// The AdmissionPolicy cares about a request if it matches _all_ Constraint.
// The MutatingAdmissionPolicy cares about a request if it matches _all_ Constraint.
// However, in order to prevent clusters from being put into an unstable state that cannot be recovered from via the API
// ValidatingAdmissionPolicy cannot match ValidatingAdmissionPolicy and ValidatingAdmissionPolicyBinding.
// Required.
@ -267,6 +267,7 @@ type ValidatingAdmissionPolicySpec struct {
//
// The expression of a variable can refer to other variables defined earlier in the list but not those after.
// Thus, Variables must be sorted by the order of first appearance and acyclic.
// +listType=atomic
// +optional
Variables []Variable
}
@ -1163,3 +1164,330 @@ type MatchCondition struct {
// Required.
Expression string
}
// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.32
// MutatingAdmissionPolicy describes an admission policy that may mutate an object.
type MutatingAdmissionPolicy struct {
metav1.TypeMeta
// Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata.
// +optional
metav1.ObjectMeta
// Specification of the desired behavior of the MutatingAdmissionPolicy.
Spec MutatingAdmissionPolicySpec
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.32
// MutatingAdmissionPolicyList is a list of MutatingAdmissionPolicy.
type MutatingAdmissionPolicyList struct {
metav1.TypeMeta
// Standard list metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
// +optional
metav1.ListMeta
// List of ValidatingAdmissionPolicy.
Items []MutatingAdmissionPolicy
}
// MutatingAdmissionPolicySpec is the specification of the desired behavior of the admission policy.
type MutatingAdmissionPolicySpec struct {
// paramKind specifies the kind of resources used to parameterize this policy.
// If absent, there are no parameters for this policy and the param CEL variable will not be provided to validation expressions.
// If paramKind refers to a non-existent kind, this policy definition is mis-configured and the FailurePolicy is applied.
// If paramKind is specified but paramRef is unset in MutatingAdmissionPolicyBinding, the params variable will be null.
// +optional
ParamKind *ParamKind
// matchConstraints specifies what resources this policy is designed to validate.
// The AdmissionPolicy cares about a request if it matches _all_ Constraints.
// However, in order to prevent clusters from being put into an unstable state that cannot be recovered from via the API
// MutatingAdmissionPolicy cannot match MutatingAdmissionPolicy and MutatingAdmissionPolicyBinding.
// Only the CREATE, UPDATE, and CONNECT operations are allowed.
// '*' matches only CREATE, UPDATE, and CONNECT.
// Required.
MatchConstraints *MatchResources
// variables contain definitions of variables that can be used in composition of other expressions.
// Each variable is defined as a named CEL expression.
// The variables defined here will be available under `variables` in other expressions of the policy
// except matchConditions because matchConditions are evaluated before the rest of the policy.
//
// The expression of a variable can refer to other variables defined earlier in the list but not those after.
// Thus, variables must be sorted by the order of first appearance and acyclic.
// +listType=atomic
// +optional
Variables []Variable
// mutations contain operations to perform on matching objects.
// mutations may not be empty; a minimum of one mutation is required.
// mutations are evaluated in order, and are reinvoked according to
// the reinvocationPolicy.
// The mutations of a policy are invoked for each binding of this policy
// and reinvocation of mutations occurs on a per binding basis.
//
// +listType=atomic
// +optional
Mutations []Mutation
// failurePolicy defines how to handle failures for the admission policy. Failures can
// 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 paramKind refers to a non-existent Kind.
// A binding is invalid if paramRef.name refers to a non-existent resource.
//
// failurePolicy does not define how validations that evaluate to false are handled.
//
// Allowed values are Ignore or Fail. Defaults to Fail.
// +optional
FailurePolicy *FailurePolicyType
// matchConditions is a list of conditions that must be met for a request to be validated.
// Match conditions filter requests that have already been matched by the matchConstraints,
// An empty list of matchConditions matches all requests.
// There are a maximum of 64 match conditions allowed.
//
// If a parameter object is provided, it can be accessed via the `params` handle in the same
// manner as validation expressions.
//
// The exact matching logic is (in order):
// 1. If ANY matchCondition evaluates to FALSE, the policy is skipped.
// 2. If ALL matchConditions evaluate to TRUE, the policy is evaluated.
// 3. If any matchCondition evaluates to an error (but none are FALSE):
// - If failurePolicy=Fail, reject the request
// - If failurePolicy=Ignore, the policy is skipped
//
// +patchMergeKey=name
// +patchStrategy=merge
// +listType=map
// +listMapKey=name
// +optional
MatchConditions []MatchCondition
// reinvocationPolicy indicates whether mutations may be called multiple times per MutatingAdmissionPolicyBinding
// as part of a single admission evaluation.
// Allowed values are "Never" and "IfNeeded".
//
// Never: These mutations will not be called more than once per binding in a single admission evaluation.
//
// IfNeeded: These mutations may be invoked more than once per binding for a single admission request and there is no guarantee of
// order with respect to other admission plugins, admission webhooks, bindings of this policy and admission policies. Mutations are only
// reinvoked when mutations change the object after this mutation is invoked.
// Required.
ReinvocationPolicy ReinvocationPolicyType
}
// Mutation specifies the operation that performs a Mutation.
type Mutation struct {
// patchType indicates the patch strategy used.
// Allowed values are "ApplyConfiguration" and "JSONPatch".
// Required.
//
// +unionDiscriminator
PatchType PatchType
// applyConfiguration defines the desired configuration values of an object.
// The configuration is applied to the admission object using
// [structured merge diff](https://github.com/kubernetes-sigs/structured-merge-diff).
// A CEL expression is used to create apply configuration.
ApplyConfiguration *ApplyConfiguration
// jsonPatch defines a [JSON patch](https://jsonpatch.com/) to perform a mutation to the object.
// A CEL expression is used to create the JSON patch.
JSONPatch *JSONPatch
}
// PatchType specifies the type of patch operation for a mutation.
// +enum
type PatchType string
const (
// ApplyConfiguration indicates that the mutation is using apply configuration to mutate the object.
PatchTypeApplyConfiguration PatchType = "ApplyConfiguration"
// JSONPatch indicates that the object is mutated through JSON Patch.
PatchTypeJSONPatch PatchType = "JSONPatch"
)
// ApplyConfiguration defines the desired configuration values of an object.
type ApplyConfiguration struct {
// expression will be evaluated by CEL to create an apply configuration.
// ref: https://github.com/google/cel-spec
//
// Apply configurations are declared in CEL using object initialization. For example, this CEL expression
// returns an apply configuration to set a single field:
//
// Object{
// spec: Object.spec{
// serviceAccountName: "example"
// }
// }
//
// Apply configurations may not modify atomic structs, maps or arrays due to the risk of accidental deletion of
// values not included in the apply configuration.
//
// CEL expressions have access to the object types needed to create apply configurations:
// - 'Object' - CEL type of the resource object.
// - 'Object.<fieldName>' - CEL type of object field (such as 'Object.spec')
// - 'Object.<fieldName1>.<fieldName2>...<fieldNameN>` - CEL type of nested field (such as 'Object.spec.containers')
//
// CEL expressions have access to the contents of the API request, 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.
// - 'oldObject' - The existing object. The value is null for CREATE requests.
// - '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.
// - 'namespaceObject' - The namespace object that the incoming object belongs to. The value is null for cluster-scoped resources.
// - 'variables' - Map of composited variables, from its name to its lazily evaluated value.
// For example, a variable named 'foo' can be accessed as 'variables.foo'.
// - '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
// - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the
// request resource.
//
// The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the
// object. No other metadata properties are accessible.
//
// Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible.
// Required.
Expression string
}
// JSONPatch defines a JSON Patch.
type JSONPatch struct {
// expression will be evaluated by CEL to create a [JSON patch](https://jsonpatch.com/).
// ref: https://github.com/google/cel-spec
//
// expression must return an array of JSONPatch values.
//
// For example, this CEL expression returns a JSON patch to conditionally modify a value:
//
// [
// JSONPatch{op: "test", path: "/spec/example", value: "Red"},
// JSONPatch{op: "replace", path: "/spec/example", value: "Green"}
// ]
//
// To define an object for the patch value, use Object types. For example:
//
// [
// JSONPatch{
// op: "add",
// path: "/spec/selector",
// value: Object.spec.selector{matchLabels: {"environment": "test"}}
// }
// ]
//
// To use strings containing '/' and '~' as JSONPatch path keys, use "jsonpatch.escapeKey". For example:
//
// [
// JSONPatch{
// op: "add",
// path: "/metadata/labels/" + jsonpatch.escapeKey("example.com/environment"),
// value: "test"
// },
// ]
//
// CEL expressions have access to the types needed to create JSON patches and objects:
//
// - 'JSONPatch' - CEL type of JSON Patch operations. JSONPatch has the fields 'op', 'from', 'path' and 'value'.
// See [JSON patch](https://jsonpatch.com/) for more details. The 'value' field may be set to any of: string,
// integer, array, map or object. If set, the 'path' and 'from' fields must be set to a
// [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901/) string, where the 'jsonpatch.escapeKey()' CEL
// function may be used to escape path keys containing '/' and '~'.
// - 'Object' - CEL type of the resource object.
// - 'Object.<fieldName>' - CEL type of object field (such as 'Object.spec')
// - 'Object.<fieldName1>.<fieldName2>...<fieldNameN>` - CEL type of nested field (such as 'Object.spec.containers')
//
// CEL expressions have access to the contents of the API request, 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.
// - 'oldObject' - The existing object. The value is null for CREATE requests.
// - '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.
// - 'namespaceObject' - The namespace object that the incoming object belongs to. The value is null for cluster-scoped resources.
// - 'variables' - Map of composited variables, from its name to its lazily evaluated value.
// For example, a variable named 'foo' can be accessed as 'variables.foo'.
// - '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
// - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the
// request resource.
//
// CEL expressions have access to [Kubernetes CEL function libraries](https://kubernetes.io/docs/reference/using-api/cel/#cel-options-language-features-and-libraries)
// as well as:
//
// - 'jsonpatch.escapeKey' - Performs JSONPatch key escaping. '~' and '/' are escaped as '~0' and `~1' respectively).
//
//
// Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible.
// Required.
Expression string
}
// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.32
// MutatingAdmissionPolicyBinding binds the MutatingAdmissionPolicy with parametrized resources.
// MutatingAdmissionPolicyBinding and the optional parameter resource together define how cluster administrators
// configure policies for clusters.
//
// For a given admission request, each binding will cause its policy to be
// evaluated N times, where N is 1 for policies/bindings that don't use
// params, otherwise N is the number of parameters selected by the binding.
// Each evaluation is constrained by a [runtime cost budget](https://kubernetes.io/docs/reference/using-api/cel/#runtime-cost-budget).
//
// Adding/removing policies, bindings, or params can not affect whether a
// given (policy, binding, param) combination is within its own CEL budget.
type MutatingAdmissionPolicyBinding struct {
metav1.TypeMeta
// Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata.
// +optional
metav1.ObjectMeta
// Specification of the desired behavior of the MutatingAdmissionPolicyBinding.
Spec MutatingAdmissionPolicyBindingSpec
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.32
// MutatingAdmissionPolicyBindingList is a list of MutatingAdmissionPolicyBinding.
type MutatingAdmissionPolicyBindingList struct {
metav1.TypeMeta
// Standard list metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
// +optional
metav1.ListMeta
// List of PolicyBinding.
Items []MutatingAdmissionPolicyBinding
}
// MutatingAdmissionPolicyBindingSpec is the specification of the MutatingAdmissionPolicyBinding.
type MutatingAdmissionPolicyBindingSpec struct {
// policyName references a MutatingAdmissionPolicy name which the MutatingAdmissionPolicyBinding binds to.
// If the referenced resource does not exist, this binding is considered invalid and will be ignored
// Required.
PolicyName string
// paramRef specifies the parameter resource used to configure the admission control policy.
// It should point to a resource of the type specified in spec.ParamKind of the bound MutatingAdmissionPolicy.
// If the policy specifies a ParamKind and the resource referred to by ParamRef does not exist, this binding is considered mis-configured and the FailurePolicy of the MutatingAdmissionPolicy applied.
// If the policy does not specify a ParamKind then this field is ignored, and the rules are evaluated without a param.
// +optional
ParamRef *ParamRef
// matchResources limits what resources match this binding and may be mutated by it.
// Note that if matchResources matches a resource, the resource must also match a policy's matchConstraints and
// matchConditions before the resource may be mutated.
// When matchResources is unset, it does not constrain resource matching, and only the policy's matchConstraints
// and matchConditions must match for the resource to be mutated.
// Additionally, matchResources.resourceRules are optional and do not constraint matching when unset.
// Note that this is differs from MutatingAdmissionPolicy matchConstraints, where resourceRules are required.
// The CREATE, UPDATE and CONNECT operations are allowed. The DELETE operation may not be matched.
// '*' matches CREATE, UPDATE and CONNECT.
// +optional
MatchResources *MatchResources
}

View File

@ -57,3 +57,11 @@ func SetDefaults_ParamRef(obj *admissionregistrationv1alpha1.ParamRef) {
obj.ParameterNotFoundAction = &v
}
}
// SetDefaults_MutatingAdmissionPolicySpec sets defaults for MutatingAdmissionPolicySpec
func SetDefaults_MutatingAdmissionPolicySpec(obj *admissionregistrationv1alpha1.MutatingAdmissionPolicySpec) {
if obj.FailurePolicy == nil {
policy := admissionregistrationv1alpha1.Fail
obj.FailurePolicy = &policy
}
}

View File

@ -31,6 +31,7 @@ import (
func TestDefaultAdmissionPolicy(t *testing.T) {
fail := v1alpha1.Fail
never := v1alpha1.NeverReinvocationPolicy
equivalent := v1alpha1.Equivalent
allScopes := v1alpha1.AllScopes
@ -103,6 +104,42 @@ func TestDefaultAdmissionPolicy(t *testing.T) {
},
},
},
{
name: "MutatingAdmissionPolicy",
original: &v1alpha1.MutatingAdmissionPolicy{
Spec: v1alpha1.MutatingAdmissionPolicySpec{
MatchConstraints: &v1alpha1.MatchResources{},
ReinvocationPolicy: never,
Mutations: []v1alpha1.Mutation{
{
PatchType: v1alpha1.PatchTypeApplyConfiguration,
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
Expression: "fake string",
},
},
},
},
},
expected: &v1alpha1.MutatingAdmissionPolicy{
Spec: v1alpha1.MutatingAdmissionPolicySpec{
MatchConstraints: &v1alpha1.MatchResources{
MatchPolicy: &equivalent,
NamespaceSelector: &metav1.LabelSelector{},
ObjectSelector: &metav1.LabelSelector{},
},
FailurePolicy: &fail,
ReinvocationPolicy: never,
Mutations: []v1alpha1.Mutation{
{
PatchType: v1alpha1.PatchTypeApplyConfiguration,
ApplyConfiguration: &v1alpha1.ApplyConfiguration{
Expression: "fake string",
},
},
},
},
},
},
}
for _, test := range tests {

View File

@ -23,6 +23,7 @@ import (
"strings"
"sync"
"k8s.io/apimachinery/pkg/api/equality"
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
"k8s.io/apimachinery/pkg/api/validation/path"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -32,6 +33,7 @@ import (
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
"k8s.io/apiserver/pkg/admission/plugin/policy/mutating"
validatingadmissionpolicy "k8s.io/apiserver/pkg/admission/plugin/policy/validating"
"k8s.io/apiserver/pkg/admission/plugin/webhook/matchconditions"
"k8s.io/apiserver/pkg/cel"
@ -274,6 +276,8 @@ type preexistingExpressions struct {
validationExpressions sets.Set[string]
validationMessageExpressions sets.Set[string]
auditAnnotationValuesExpressions sets.Set[string]
applyConfigurationExpressions sets.Set[string]
jsonPatchExpressions sets.Set[string]
}
func newPreexistingExpressions() preexistingExpressions {
@ -282,6 +286,8 @@ func newPreexistingExpressions() preexistingExpressions {
validationExpressions: sets.New[string](),
validationMessageExpressions: sets.New[string](),
auditAnnotationValuesExpressions: sets.New[string](),
applyConfigurationExpressions: sets.New[string](),
jsonPatchExpressions: sets.New[string](),
}
}
@ -322,6 +328,22 @@ func findValidatingPolicyPreexistingExpressions(validatingPolicy *admissionregis
return preexisting
}
func findMutatingPolicyPreexistingExpressions(mutatingPolicy *admissionregistration.MutatingAdmissionPolicy) preexistingExpressions {
preexisting := newPreexistingExpressions()
for _, mc := range mutatingPolicy.Spec.MatchConditions {
preexisting.matchConditionExpressions.Insert(mc.Expression)
}
for _, v := range mutatingPolicy.Spec.Mutations {
if v.ApplyConfiguration != nil {
preexisting.applyConfigurationExpressions.Insert(v.ApplyConfiguration.Expression)
}
if v.JSONPatch != nil {
preexisting.jsonPatchExpressions.Insert(v.JSONPatch.Expression)
}
}
return preexisting
}
func validateMutatingWebhookConfiguration(e *admissionregistration.MutatingWebhookConfiguration, opts validationOptions) field.ErrorList {
allErrors := genericvalidation.ValidateObjectMeta(&e.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))
@ -495,6 +517,19 @@ var supportedValidationPolicyReason = sets.NewString(
string(metav1.StatusReasonRequestEntityTooLarge),
)
var supportedPatchType = sets.NewString(
string(admissionregistration.PatchTypeApplyConfiguration),
string(admissionregistration.PatchTypeJSONPatch),
)
// MutatatingAdmissionPolicy does not support DELETE
var supportedMutatingOperations = sets.NewString(
string(admissionregistration.OperationAll),
string(admissionregistration.Create),
string(admissionregistration.Update),
string(admissionregistration.Connect),
)
func hasWildcardOperation(operations []admissionregistration.OperationType) bool {
for _, o := range operations {
if o == admissionregistration.OperationAll {
@ -588,10 +623,21 @@ func ignoreValidatingWebhookMatchConditions(new, old []admissionregistration.Val
// ignoreValidatingAdmissionPolicyMatchConditions returns true if there have been no updates that could invalidate previously-valid match conditions
func ignoreValidatingAdmissionPolicyMatchConditions(new, old *admissionregistration.ValidatingAdmissionPolicy) bool {
if !reflect.DeepEqual(new.Spec.ParamKind, old.Spec.ParamKind) {
if !equality.Semantic.DeepEqual(new.Spec.ParamKind, old.Spec.ParamKind) {
return false
}
if !reflect.DeepEqual(new.Spec.MatchConditions, old.Spec.MatchConditions) {
if !equality.Semantic.DeepEqual(new.Spec.MatchConditions, old.Spec.MatchConditions) {
return false
}
return true
}
// ignoreMutatingAdmissionPolicyMatchConditions returns true if there have been no updates that could invalidate previously-valid match conditions
func ignoreMutatingAdmissionPolicyMatchConditions(new, old *admissionregistration.MutatingAdmissionPolicy) bool {
if !equality.Semantic.DeepEqual(new.Spec.ParamKind, old.Spec.ParamKind) {
return false
}
if !equality.Semantic.DeepEqual(new.Spec.MatchConditions, old.Spec.MatchConditions) {
return false
}
return true
@ -1158,7 +1204,7 @@ func validateValidatingAdmissionPolicyBindingSpec(spec *admissionregistration.Va
}
}
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("matchResources"))...)
allErrors = append(allErrors, validateValidationActions(spec.ValidationActions, fldPath.Child("validationActions"))...)
return allErrors
@ -1328,3 +1374,195 @@ func isCELIdentifier(name string) bool {
// | "var" | "void" | "while"
return celIdentRegex.MatchString(name) && !celReserved.Has(name)
}
// ValidateMutatingAdmissionPolicyUpdate validates update of mutating admission policy
func ValidateMutatingAdmissionPolicyUpdate(newC, oldC *admissionregistration.MutatingAdmissionPolicy) field.ErrorList {
return validateMutatingAdmissionPolicy(newC, validationOptions{
ignoreMatchConditions: ignoreMutatingAdmissionPolicyMatchConditions(newC, oldC),
preexistingExpressions: findMutatingPolicyPreexistingExpressions(oldC),
strictCostEnforcement: true,
})
}
// ValidateMutatingAdmissionPolicyBindingUpdate validates update of mutating admission policy
func ValidateMutatingAdmissionPolicyBindingUpdate(newC, oldC *admissionregistration.MutatingAdmissionPolicyBinding) field.ErrorList {
return validateMutatingAdmissionPolicyBinding(newC)
}
// ValidateMutatingAdmissionPolicy validates a MutatingAdmissionPolicy before creation.
func ValidateMutatingAdmissionPolicy(p *admissionregistration.MutatingAdmissionPolicy) field.ErrorList {
return validateMutatingAdmissionPolicy(p, validationOptions{ignoreMatchConditions: false, strictCostEnforcement: true})
}
func validateMutatingAdmissionPolicy(p *admissionregistration.MutatingAdmissionPolicy, opts validationOptions) field.ErrorList {
allErrors := genericvalidation.ValidateObjectMeta(&p.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))
allErrors = append(allErrors, validateMutatingAdmissionPolicySpec(p.ObjectMeta, &p.Spec, opts, field.NewPath("spec"))...)
return allErrors
}
func validateMutatingAdmissionPolicySpec(meta metav1.ObjectMeta, spec *admissionregistration.MutatingAdmissionPolicySpec, opts validationOptions, fldPath *field.Path) field.ErrorList {
var allErrors field.ErrorList
compiler := createCompiler(true, true)
if spec.FailurePolicy == nil {
allErrors = append(allErrors, field.Required(fldPath.Child("failurePolicy"), ""))
} else if !supportedFailurePolicies.Has(string(*spec.FailurePolicy)) {
allErrors = append(allErrors, field.NotSupported(fldPath.Child("failurePolicy"), *spec.FailurePolicy, supportedFailurePolicies.List()))
}
if spec.ParamKind != nil {
opts.allowParamsInMatchConditions = true
allErrors = append(allErrors, validateParamKind(*spec.ParamKind, fldPath.Child("paramKind"))...)
}
if spec.MatchConstraints == nil {
allErrors = append(allErrors, field.Required(fldPath.Child("matchConstraints"), ""))
} else {
allErrors = append(allErrors, validateMatchResources(spec.MatchConstraints, fldPath.Child("matchConstraints"))...)
// at least one resourceRule must be defined to provide type information
if len(spec.MatchConstraints.ResourceRules) == 0 {
allErrors = append(allErrors, field.Required(fldPath.Child("matchConstraints", "resourceRules"), ""))
}
// It is only possible to mutate create and update requests
for _, rule := range spec.MatchConstraints.ResourceRules {
for _, op := range rule.RuleWithOperations.Operations {
if !supportedMutatingOperations.Has(string(op)) {
allErrors = append(allErrors, field.NotSupported(fldPath.Child("matchConstraints", "resourceRules", "operations"), op, supportedMutatingOperations.List()))
}
}
}
}
if !opts.ignoreMatchConditions {
allErrors = append(allErrors, validateMatchConditions(spec.MatchConditions, opts, fldPath.Child("matchConditions"))...)
}
for i, variable := range spec.Variables {
allErrors = append(allErrors, validateVariable(compiler, &variable, spec.ParamKind, opts, fldPath.Child("variables").Index(i))...)
}
if len(spec.Mutations) == 0 {
allErrors = append(allErrors, field.Required(fldPath.Child("mutations"), "mutations must contain at least one item"))
} else {
for i, mutation := range spec.Mutations {
allErrors = append(allErrors, validateMutation(compiler, &mutation, spec.ParamKind, opts, fldPath.Child("mutations").Index(i))...)
}
}
if len(spec.ReinvocationPolicy) == 0 {
allErrors = append(allErrors, field.Required(fldPath.Child("reinvocationPolicy"), ""))
} else if !supportedReinvocationPolicies.Has(string(spec.ReinvocationPolicy)) {
allErrors = append(allErrors, field.NotSupported(fldPath.Child("reinvocationPolicy"), spec.ReinvocationPolicy, supportedReinvocationPolicies.List()))
}
return allErrors
}
func validateMutation(compiler plugincel.Compiler, m *admissionregistration.Mutation, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) (allErrors field.ErrorList) {
if len(m.PatchType) == 0 {
allErrors = append(allErrors, field.Required(fldPath.Child("patchType"), ""))
} else {
switch m.PatchType {
case admissionregistration.PatchTypeJSONPatch:
if m.JSONPatch == nil {
allErrors = append(allErrors, field.Required(fldPath.Child("jsonPatch"), "must be specified when patchType is JSONPatch"))
} else {
allErrors = append(allErrors, validateJSONPatch(compiler, m.JSONPatch, paramKind, opts, fldPath.Child("jsonPatch"))...)
}
if m.ApplyConfiguration != nil {
allErrors = append(allErrors, field.Invalid(fldPath.Child("applyConfiguration"), "{applyConfiguration}", "must not be specified when patchType is JSONPatch"))
}
case admissionregistration.PatchTypeApplyConfiguration:
if m.ApplyConfiguration == nil {
allErrors = append(allErrors, field.Required(fldPath.Child("applyConfiguration"), "must be specified when patchType is ApplyConfiguration"))
} else {
allErrors = append(allErrors, validateApplyConfiguration(compiler, m.ApplyConfiguration, paramKind, opts, fldPath.Child("applyConfiguration"))...)
}
if m.JSONPatch != nil {
allErrors = append(allErrors, field.Invalid(fldPath.Child("jsonPatch"), "{jsonPatch}", "must not be specified when patchType is ApplyConfiguration"))
}
default:
allErrors = append(allErrors, field.NotSupported(fldPath.Child("patchType"), m.PatchType, supportedPatchType.List()))
}
}
return allErrors
}
func validateApplyConfiguration(compiler plugincel.Compiler, applyConfig *admissionregistration.ApplyConfiguration, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) (allErrors field.ErrorList) {
trimmedExpression := strings.TrimSpace(applyConfig.Expression)
if len(trimmedExpression) == 0 {
allErrors = append(allErrors, field.Required(fldPath.Child("expression"), ""))
} else {
envType := environment.NewExpressions
if opts.preexistingExpressions.applyConfigurationExpressions.Has(applyConfig.Expression) {
envType = environment.StoredExpressions
}
accessor := &mutating.ApplyConfigurationCondition{
Expression: trimmedExpression,
}
opts := plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true, StrictCost: true, HasPatchTypes: true}
result := compiler.CompileCELExpression(accessor, opts, envType)
if result.Error != nil {
allErrors = append(allErrors, convertCELErrorToValidationError(fldPath.Child("expression"), accessor, result.Error))
}
}
return allErrors
}
func validateJSONPatch(compiler plugincel.Compiler, jsonPatch *admissionregistration.JSONPatch, paramKind *admissionregistration.ParamKind, opts validationOptions, fldPath *field.Path) (allErrors field.ErrorList) {
trimmedExpression := strings.TrimSpace(jsonPatch.Expression)
if len(trimmedExpression) == 0 {
allErrors = append(allErrors, field.Required(fldPath.Child("expression"), ""))
} else {
envType := environment.NewExpressions
if opts.preexistingExpressions.applyConfigurationExpressions.Has(jsonPatch.Expression) {
envType = environment.StoredExpressions
}
accessor := &mutating.JSONPatchCondition{
Expression: trimmedExpression,
}
opts := plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true, StrictCost: true, HasPatchTypes: true}
result := compiler.CompileCELExpression(accessor, opts, envType)
if result.Error != nil {
allErrors = append(allErrors, convertCELErrorToValidationError(fldPath.Child("expression"), accessor, result.Error))
}
}
return allErrors
}
// ValidateMutatingAdmissionPolicyBinding validates a MutatingAdmissionPolicyBinding before create.
func ValidateMutatingAdmissionPolicyBinding(pb *admissionregistration.MutatingAdmissionPolicyBinding) field.ErrorList {
return validateMutatingAdmissionPolicyBinding(pb)
}
func validateMutatingAdmissionPolicyBinding(pb *admissionregistration.MutatingAdmissionPolicyBinding) field.ErrorList {
allErrors := genericvalidation.ValidateObjectMeta(&pb.ObjectMeta, false, genericvalidation.NameIsDNSSubdomain, field.NewPath("metadata"))
allErrors = append(allErrors, validateMutatingAdmissionPolicyBindingSpec(&pb.Spec, field.NewPath("spec"))...)
return allErrors
}
func validateMutatingAdmissionPolicyBindingSpec(spec *admissionregistration.MutatingAdmissionPolicyBindingSpec, fldPath *field.Path) field.ErrorList {
var allErrors field.ErrorList
if len(spec.PolicyName) == 0 {
allErrors = append(allErrors, field.Required(fldPath.Child("policyName"), ""))
} else {
for _, msg := range genericvalidation.NameIsDNSSubdomain(spec.PolicyName, false) {
allErrors = append(allErrors, field.Invalid(fldPath.Child("policyName"), spec.PolicyName, msg))
}
}
allErrors = append(allErrors, validateParamRef(spec.ParamRef, fldPath.Child("paramRef"))...)
allErrors = append(allErrors, validateMatchResources(spec.MatchResources, fldPath.Child("matchResources"))...)
if spec.MatchResources != nil {
// It is only possible to mutate create and update requests
for _, rule := range spec.MatchResources.ResourceRules {
for _, op := range rule.RuleWithOperations.Operations {
if !supportedMutatingOperations.Has(string(op)) {
allErrors = append(allErrors, field.NotSupported(fldPath.Child("matchResources", "resourceRules", "operations"), op, supportedMutatingOperations.List()))
}
}
}
}
return allErrors
}

File diff suppressed because it is too large Load Diff

View File

@ -27,6 +27,7 @@ import (
"k8s.io/apiserver/pkg/storage/storagebackend"
version "k8s.io/component-base/version"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/admissionregistration"
"k8s.io/kubernetes/pkg/apis/apps"
"k8s.io/kubernetes/pkg/apis/certificates"
"k8s.io/kubernetes/pkg/apis/coordination"
@ -75,6 +76,8 @@ func NewStorageFactoryConfig() *StorageFactoryConfig {
coordination.Resource("leasecandidates").WithVersion("v1alpha1"),
networking.Resource("ipaddresses").WithVersion("v1beta1"),
networking.Resource("servicecidrs").WithVersion("v1beta1"),
admissionregistration.Resource("mutatingadmissionpolicies").WithVersion("v1alpha1"),
admissionregistration.Resource("mutatingadmissionpolicybindings").WithVersion("v1alpha1"),
certificates.Resource("clustertrustbundles").WithVersion("v1alpha1"),
storage.Resource("volumeattributesclasses").WithVersion("v1beta1"),
storagemigration.Resource("storagemigrations").WithVersion("v1alpha1"),

View File

@ -596,6 +596,24 @@ func AddHandlers(h printers.PrintHandler) {
_ = h.TableHandler(validatingAdmissionPolicyBinding, printValidatingAdmissionPolicyBinding)
_ = h.TableHandler(validatingAdmissionPolicyBinding, printValidatingAdmissionPolicyBindingList)
mutatingAdmissionPolicy := []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
{Name: "Mutations", Type: "integer", Description: "Mutation indicates the number of mutations rules defined in this configuration"},
{Name: "ParamKind", Type: "string", Description: "ParamKind specifies the kind of resources used to parameterize this policy"},
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
}
_ = h.TableHandler(mutatingAdmissionPolicy, printMutatingAdmissionPolicy)
_ = h.TableHandler(mutatingAdmissionPolicy, printMutatingAdmissionPolicyList)
mutatingAdmissionPolicyBinding := []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
{Name: "PolicyName", Type: "string", Description: "PolicyName indicates the policy definition which the policy binding binded to"},
{Name: "ParamRef", Type: "string", Description: "ParamRef indicates the param resource which sets the configration param"},
{Name: "Age", Type: "string", Description: metav1.ObjectMeta{}.SwaggerDoc()["creationTimestamp"]},
}
_ = h.TableHandler(mutatingAdmissionPolicyBinding, printMutatingAdmissionPolicyBinding)
_ = h.TableHandler(mutatingAdmissionPolicyBinding, printMutatingAdmissionPolicyBindingList)
flowSchemaColumnDefinitions := []metav1.TableColumnDefinition{
{Name: "Name", Type: "string", Format: "name", Description: metav1.ObjectMeta{}.SwaggerDoc()["name"]},
{Name: "PriorityLevel", Type: "string", Description: flowcontrolv1.PriorityLevelConfigurationReference{}.SwaggerDoc()["name"]},
@ -1765,6 +1783,64 @@ func printValidatingAdmissionPolicyBindingList(list *admissionregistration.Valid
return rows, nil
}
func printMutatingAdmissionPolicy(obj *admissionregistration.MutatingAdmissionPolicy, options printers.GenerateOptions) ([]metav1.TableRow, error) {
row := metav1.TableRow{
Object: runtime.RawExtension{Object: obj},
}
paramKind := "<unset>"
if obj.Spec.ParamKind != nil {
paramKind = obj.Spec.ParamKind.APIVersion + "/" + obj.Spec.ParamKind.Kind
}
row.Cells = append(row.Cells, obj.Name, int64(len(obj.Spec.Mutations)), paramKind, translateTimestampSince(obj.CreationTimestamp))
return []metav1.TableRow{row}, nil
}
func printMutatingAdmissionPolicyList(list *admissionregistration.MutatingAdmissionPolicyList, options printers.GenerateOptions) ([]metav1.TableRow, error) {
rows := make([]metav1.TableRow, 0, len(list.Items))
for i := range list.Items {
r, err := printMutatingAdmissionPolicy(&list.Items[i], options)
if err != nil {
return nil, err
}
rows = append(rows, r...)
}
return rows, nil
}
func printMutatingAdmissionPolicyBinding(obj *admissionregistration.MutatingAdmissionPolicyBinding, options printers.GenerateOptions) ([]metav1.TableRow, error) {
row := metav1.TableRow{
Object: runtime.RawExtension{Object: obj},
}
paramName := "<unset>"
if pr := obj.Spec.ParamRef; pr != nil {
if len(pr.Name) > 0 {
if pr.Namespace != "" {
paramName = pr.Namespace + "/" + pr.Name
} else {
// Can't tell from here if param is cluster-scoped, so all
// params without names get * namespace
paramName = "*/" + pr.Name
}
} else if pr.Selector != nil {
paramName = pr.Selector.String()
}
}
row.Cells = append(row.Cells, obj.Name, obj.Spec.PolicyName, paramName, translateTimestampSince(obj.CreationTimestamp))
return []metav1.TableRow{row}, nil
}
func printMutatingAdmissionPolicyBindingList(list *admissionregistration.MutatingAdmissionPolicyBindingList, options printers.GenerateOptions) ([]metav1.TableRow, error) {
rows := make([]metav1.TableRow, 0, len(list.Items))
for i := range list.Items {
r, err := printMutatingAdmissionPolicyBinding(&list.Items[i], options)
if err != nil {
return nil, err
}
rows = append(rows, r...)
}
return rows, nil
}
func printNamespace(obj *api.Namespace, options printers.GenerateOptions) ([]metav1.TableRow, error) {
row := metav1.TableRow{
Object: runtime.RawExtension{Object: obj},

View File

@ -7025,6 +7025,18 @@ func TestTableRowDeepCopyShouldNotPanic(t *testing.T) {
return printValidatingAdmissionPolicyBinding(&admissionregistration.ValidatingAdmissionPolicyBinding{}, printers.GenerateOptions{})
},
},
{
name: "MutatingAdmissionPolicy",
printer: func() ([]metav1.TableRow, error) {
return printMutatingAdmissionPolicy(&admissionregistration.MutatingAdmissionPolicy{}, printers.GenerateOptions{})
},
},
{
name: "MutatingAdmissionPolicyBinding",
printer: func() ([]metav1.TableRow, error) {
return printMutatingAdmissionPolicyBinding(&admissionregistration.MutatingAdmissionPolicyBinding{}, printers.GenerateOptions{})
},
},
{
name: "Namespace",
printer: func() ([]metav1.TableRow, error) {

View File

@ -0,0 +1,105 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutatingadmissionpolicy
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/kubernetes/pkg/apis/admissionregistration"
rbacregistry "k8s.io/kubernetes/pkg/registry/rbac"
)
func (v *mutatingAdmissionPolicyStrategy) authorizeCreate(ctx context.Context, obj runtime.Object) error {
policy := obj.(*admissionregistration.MutatingAdmissionPolicy)
if policy.Spec.ParamKind == nil {
// no paramRef in new object
return nil
}
return v.authorize(ctx, policy)
}
func (v *mutatingAdmissionPolicyStrategy) authorizeUpdate(ctx context.Context, obj, old runtime.Object) error {
policy := obj.(*admissionregistration.MutatingAdmissionPolicy)
if policy.Spec.ParamKind == nil {
// no paramRef in new object
return nil
}
oldPolicy := old.(*admissionregistration.MutatingAdmissionPolicy)
if oldPolicy.Spec.ParamKind != nil && *oldPolicy.Spec.ParamKind == *policy.Spec.ParamKind {
// identical paramKind to old object
return nil
}
return v.authorize(ctx, policy)
}
func (v *mutatingAdmissionPolicyStrategy) authorize(ctx context.Context, policy *admissionregistration.MutatingAdmissionPolicy) error {
if v.authorizer == nil || policy.Spec.ParamKind == nil {
return nil
}
// for superuser, skip all checks
if rbacregistry.EscalationAllowed(ctx) {
return nil
}
user, ok := genericapirequest.UserFrom(ctx)
if !ok {
return fmt.Errorf("cannot identify user to authorize read access to paramKind resources")
}
paramKind := policy.Spec.ParamKind
// default to requiring permissions on all group/version/resources
resource, apiGroup, apiVersion := "*", "*", "*"
if gv, err := schema.ParseGroupVersion(paramKind.APIVersion); err == nil {
// we only need to authorize the parsed group/version
apiGroup = gv.Group
apiVersion = gv.Version
if gvr, err := v.resourceResolver.Resolve(gv.WithKind(paramKind.Kind)); err == nil {
// we only need to authorize the resolved resource
resource = gvr.Resource
}
}
// require that the user can read (verb "get") the referred kind.
attrs := authorizer.AttributesRecord{
User: user,
Verb: "get",
ResourceRequest: true,
Name: "*",
Namespace: "*",
APIGroup: apiGroup,
APIVersion: apiVersion,
Resource: resource,
}
d, _, err := v.authorizer.Authorize(ctx, attrs)
if err != nil {
return err
}
if d != authorizer.DecisionAllow {
return fmt.Errorf(`user %v must have "get" permission on all objects of the referenced paramKind (kind=%s, apiVersion=%s)`, user, paramKind.Kind, paramKind.APIVersion)
}
return nil
}

View File

@ -0,0 +1,131 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutatingadmissionpolicy
import (
"context"
"testing"
"k8s.io/apimachinery/pkg/api/meta"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/kubernetes/pkg/apis/admissionregistration"
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
)
func TestAuthorization(t *testing.T) {
for _, tc := range []struct {
name string
userInfo user.Info
obj *admissionregistration.MutatingAdmissionPolicy
auth AuthFunc
resourceResolver resolver.ResourceResolverFunc
expectErr bool
}{
{
name: "superuser",
userInfo: &user.DefaultInfo{Groups: []string{user.SystemPrivilegedGroup}},
expectErr: false, // success despite always-denying authorizer
obj: validMutatingAdmissionPolicy(),
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
return authorizer.DecisionDeny, "", nil
},
},
{
name: "authorized",
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
obj: validMutatingAdmissionPolicy(),
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
if a.GetResource() == "replicalimits" {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "", nil
},
resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
return schema.GroupVersionResource{
Group: "rules.example.com",
Version: "v1",
Resource: "replicalimits",
}, nil
},
expectErr: false,
},
{
name: "denied",
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
obj: validMutatingAdmissionPolicy(),
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
if a.GetResource() == "configmaps" {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "", nil
},
resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
return schema.GroupVersionResource{
Group: "rules.example.com",
Version: "v1",
Resource: "replicalimits",
}, nil
},
expectErr: true,
},
{
name: "param not found",
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
obj: validMutatingAdmissionPolicy(),
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
if a.GetResource() == "replicalimits" {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "", nil
},
resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
return schema.GroupVersionResource{}, &meta.NoKindMatchError{GroupKind: gvk.GroupKind(), SearchedVersions: []string{gvk.Version}}
},
expectErr: true,
},
} {
t.Run(tc.name, func(t *testing.T) {
strategy := NewStrategy(tc.auth, tc.resourceResolver)
t.Run("create", func(t *testing.T) {
ctx := request.WithUser(context.Background(), tc.userInfo)
errs := strategy.Validate(ctx, validMutatingAdmissionPolicy())
if len(errs) > 0 != tc.expectErr {
t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs)
}
})
t.Run("update", func(t *testing.T) {
ctx := request.WithUser(context.Background(), tc.userInfo)
obj := validMutatingAdmissionPolicy()
objWithUpdatedParamKind := obj.DeepCopy()
objWithUpdatedParamKind.Spec.ParamKind.APIVersion += "1"
errs := strategy.ValidateUpdate(ctx, obj, objWithUpdatedParamKind)
if len(errs) > 0 != tc.expectErr {
t.Errorf("expected error: %v but got error: %v", tc.expectErr, errs)
}
})
})
}
}
type AuthFunc func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error)
func (f AuthFunc) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
return f(ctx, a)
}

View File

@ -0,0 +1,17 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutatingadmissionpolicy // import "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicy"

View File

@ -0,0 +1,73 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package storage
import (
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/kubernetes/pkg/apis/admissionregistration"
"k8s.io/kubernetes/pkg/printers"
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
"k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicy"
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
)
// REST implements a RESTStorage for MutatingAdmissionPolicy against etcd
type REST struct {
*genericregistry.Store
}
var groupResource = admissionregistration.Resource("mutatingadmissionpolicies")
// NewREST returns the RESTStorage objects that will work against MutatingAdmissionPolicy.
func NewREST(optsGetter generic.RESTOptionsGetter, authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) (*REST, error) {
r := &REST{}
strategy := mutatingadmissionpolicy.NewStrategy(authorizer, resourceResolver)
store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &admissionregistration.MutatingAdmissionPolicy{} },
NewListFunc: func() runtime.Object { return &admissionregistration.MutatingAdmissionPolicyList{} },
ObjectNameFunc: func(obj runtime.Object) (string, error) {
return obj.(*admissionregistration.MutatingAdmissionPolicy).Name, nil
},
DefaultQualifiedResource: groupResource,
SingularQualifiedResource: admissionregistration.Resource("mutatingadmissionpolicy"),
CreateStrategy: strategy,
UpdateStrategy: strategy,
DeleteStrategy: strategy,
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
}
options := &generic.StoreOptions{RESTOptions: optsGetter}
if err := store.CompleteWithOptions(options); err != nil {
return nil, err
}
r.Store = store
return r, nil
}
// Implement CategoriesProvider
var _ rest.CategoriesProvider = &REST{}
// Categories implements the CategoriesProvider interface. Returns a list of categories a resource is part of.
func (r *REST) Categories() []string {
return []string{"api-extensions"}
}

View File

@ -0,0 +1,252 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package storage
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/generic"
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
"k8s.io/kubernetes/pkg/apis/admissionregistration"
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
"k8s.io/kubernetes/pkg/registry/registrytest"
// Ensure that admissionregistration package is initialized.
_ "k8s.io/kubernetes/pkg/apis/admissionregistration/install"
)
func TestCreate(t *testing.T) {
storage, server := newInsecureStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
configuration := validMutatingAdmissionPolicy()
test.TestCreate(
// valid
configuration,
// invalid
newMutatingAdmissionPolicy(""),
)
}
func TestUpdate(t *testing.T) {
storage, server := newInsecureStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestUpdate(
// valid
validMutatingAdmissionPolicy(),
// updateFunc
func(obj runtime.Object) runtime.Object {
object := obj.(*admissionregistration.MutatingAdmissionPolicy)
object.Labels = map[string]string{"c": "d"}
return object
},
// invalid updateFunc
func(obj runtime.Object) runtime.Object {
object := obj.(*admissionregistration.MutatingAdmissionPolicy)
object.Name = ""
return object
},
)
}
func TestGet(t *testing.T) {
storage, server := newInsecureStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestGet(validMutatingAdmissionPolicy())
}
func TestList(t *testing.T) {
storage, server := newInsecureStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestList(validMutatingAdmissionPolicy())
}
func TestDelete(t *testing.T) {
storage, server := newInsecureStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestDelete(validMutatingAdmissionPolicy())
}
func TestWatch(t *testing.T) {
storage, server := newInsecureStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestWatch(
validMutatingAdmissionPolicy(),
[]labels.Set{},
[]labels.Set{
{"hoo": "bar"},
},
[]fields.Set{
{"metadata.name": "foo"},
},
[]fields.Set{
{"metadata.name": "nomatch"},
},
)
}
func validMutatingAdmissionPolicy() *admissionregistration.MutatingAdmissionPolicy {
return &admissionregistration.MutatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: admissionregistration.MutatingAdmissionPolicySpec{
FailurePolicy: func() *admissionregistration.FailurePolicyType {
r := admissionregistration.FailurePolicyType("Fail")
return &r
}(),
ParamKind: &admissionregistration.ParamKind{
APIVersion: "rules.example.com/v1",
Kind: "ReplicaLimit",
},
ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy,
Mutations: []admissionregistration.Mutation{
{
PatchType: admissionregistration.PatchTypeApplyConfiguration,
ApplyConfiguration: &admissionregistration.ApplyConfiguration{
Expression: `Object{
spec: Object.spec{
replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas
}
}`,
},
},
},
MatchConstraints: &admissionregistration.MatchResources{
MatchPolicy: func() *admissionregistration.MatchPolicyType {
r := admissionregistration.MatchPolicyType("Exact")
return &r
}(),
ResourceRules: []admissionregistration.NamedRuleWithOperations{
{
RuleWithOperations: admissionregistration.RuleWithOperations{
Operations: []admissionregistration.OperationType{"CREATE"},
Rule: admissionregistration.Rule{
APIGroups: []string{"a"},
APIVersions: []string{"a"},
Resources: []string{"a"},
},
},
},
},
ObjectSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
},
},
}
}
func newMutatingAdmissionPolicy(name string) *admissionregistration.MutatingAdmissionPolicy {
ignore := admissionregistration.Ignore
return &admissionregistration.MutatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: map[string]string{"foo": "bar"},
},
Spec: admissionregistration.MutatingAdmissionPolicySpec{
ParamKind: &admissionregistration.ParamKind{
APIVersion: "rules.example.com/v1",
Kind: "ReplicaLimit",
},
ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy,
Mutations: []admissionregistration.Mutation{
{
PatchType: admissionregistration.PatchTypeApplyConfiguration,
ApplyConfiguration: &admissionregistration.ApplyConfiguration{
Expression: `Object{
spec: Object.spec{
replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas
}
}`,
},
},
},
MatchConstraints: &admissionregistration.MatchResources{
ResourceRules: []admissionregistration.NamedRuleWithOperations{
{
RuleWithOperations: admissionregistration.RuleWithOperations{
Operations: []admissionregistration.OperationType{"CREATE"},
Rule: admissionregistration.Rule{
APIGroups: []string{"a"},
APIVersions: []string{"a"},
Resources: []string{"a"},
},
},
},
},
},
FailurePolicy: &ignore,
},
}
}
func newInsecureStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
return newStorage(t, nil, replicaLimitsResolver)
}
func newStorage(t *testing.T, authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) (*REST, *etcd3testing.EtcdTestServer) {
etcdStorage, server := registrytest.NewEtcdStorageForResource(t, admissionregistration.Resource("mutatingadmissionpolicies"))
restOptions := generic.RESTOptions{
StorageConfig: etcdStorage,
Decorator: generic.UndecoratedStorage,
DeleteCollectionWorkers: 1,
ResourcePrefix: "mutatingadmissionpolicies"}
storage, err := NewREST(restOptions, authorizer, resourceResolver)
if err != nil {
t.Fatalf("unexpected error from REST storage: %v", err)
}
return storage, server
}
func TestCategories(t *testing.T) {
storage, server := newInsecureStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
expected := []string{"api-extensions"}
registrytest.AssertCategories(t, storage, expected)
}
var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
return schema.GroupVersionResource{
Group: "rules.example.com",
Version: "v1",
Resource: "replicalimits",
}, nil
}

View File

@ -0,0 +1,122 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutatingadmissionpolicy
import (
"context"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/admissionregistration"
"k8s.io/kubernetes/pkg/apis/admissionregistration/validation"
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
)
// mutatingAdmissionPolicyStrategy implements verification logic for MutatingAdmissionPolicy.
type mutatingAdmissionPolicyStrategy struct {
runtime.ObjectTyper
names.NameGenerator
authorizer authorizer.Authorizer
resourceResolver resolver.ResourceResolver
}
// NewStrategy is the default logic that applies when creating and updating MutatingAdmissionPolicy objects.
func NewStrategy(authorizer authorizer.Authorizer, resourceResolver resolver.ResourceResolver) *mutatingAdmissionPolicyStrategy {
return &mutatingAdmissionPolicyStrategy{
ObjectTyper: legacyscheme.Scheme,
NameGenerator: names.SimpleNameGenerator,
authorizer: authorizer,
resourceResolver: resourceResolver,
}
}
// NamespaceScoped returns false because MutatingAdmissionPolicy is cluster-scoped resource.
func (v *mutatingAdmissionPolicyStrategy) NamespaceScoped() bool {
return false
}
// PrepareForCreate clears the status of an MutatingAdmissionPolicy before creation.
func (v *mutatingAdmissionPolicyStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
ic := obj.(*admissionregistration.MutatingAdmissionPolicy)
ic.Generation = 1
}
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
func (v *mutatingAdmissionPolicyStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
newIC := obj.(*admissionregistration.MutatingAdmissionPolicy)
oldIC := old.(*admissionregistration.MutatingAdmissionPolicy)
// Any changes to the spec increment the generation number, any changes to the
// status should reflect the generation number of the corresponding object.
// See metav1.ObjectMeta description for more information on Generation.
if !apiequality.Semantic.DeepEqual(oldIC.Spec, newIC.Spec) {
newIC.Generation = oldIC.Generation + 1
}
}
// Validate validates a new MutatingAdmissionPolicy.
func (v *mutatingAdmissionPolicyStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
errs := validation.ValidateMutatingAdmissionPolicy(obj.(*admissionregistration.MutatingAdmissionPolicy))
if len(errs) == 0 {
// if the object is well-formed, also authorize the paramKind
if err := v.authorizeCreate(ctx, obj); err != nil {
errs = append(errs, field.Forbidden(field.NewPath("spec", "paramKind"), err.Error()))
}
}
return errs
}
// WarningsOnCreate returns warnings for the creation of the given object.
func (v *mutatingAdmissionPolicyStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
return nil
}
// Canonicalize normalizes the object after validation.
func (v *mutatingAdmissionPolicyStrategy) Canonicalize(obj runtime.Object) {
}
// AllowCreateOnUpdate is false for MutatingAdmissionPolicy; this means you may not create one with a PUT request.
func (v *mutatingAdmissionPolicyStrategy) AllowCreateOnUpdate() bool {
return false
}
// ValidateUpdate is the default update validation for an end user.
func (v *mutatingAdmissionPolicyStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
errs := validation.ValidateMutatingAdmissionPolicyUpdate(obj.(*admissionregistration.MutatingAdmissionPolicy), old.(*admissionregistration.MutatingAdmissionPolicy))
if len(errs) == 0 {
// if the object is well-formed, also authorize the paramKind
if err := v.authorizeUpdate(ctx, obj, old); err != nil {
errs = append(errs, field.Forbidden(field.NewPath("spec", "paramKind"), err.Error()))
}
}
return errs
}
// WarningsOnUpdate returns warnings for the given update.
func (v *mutatingAdmissionPolicyStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
return nil
}
// AllowUnconditionalUpdate is the default update policy for MutatingAdmissionPolicy objects. Status update should
// only be allowed if version match.
func (v *mutatingAdmissionPolicyStrategy) AllowUnconditionalUpdate() bool {
return false
}

View File

@ -0,0 +1,114 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutatingadmissionpolicy
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/kubernetes/pkg/apis/admissionregistration"
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
)
func TestMutatingAdmissionPolicyStrategy(t *testing.T) {
strategy := NewStrategy(nil, replicaLimitsResolver)
ctx := genericapirequest.NewDefaultContext()
if strategy.NamespaceScoped() {
t.Error("MutatingAdmissionPolicy strategy must be cluster scoped")
}
if strategy.AllowCreateOnUpdate() {
t.Errorf("MutatingAdmissionPolicy should not allow create on update")
}
configuration := validMutatingAdmissionPolicy()
strategy.PrepareForCreate(ctx, configuration)
errs := strategy.Validate(ctx, configuration)
if len(errs) != 0 {
t.Errorf("Unexpected error mutating %v", errs)
}
invalidConfiguration := &admissionregistration.MutatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{Name: ""},
}
strategy.PrepareForUpdate(ctx, invalidConfiguration, configuration)
errs = strategy.ValidateUpdate(ctx, invalidConfiguration, configuration)
if len(errs) == 0 {
t.Errorf("Expected a validation error")
}
}
var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
return schema.GroupVersionResource{
Group: "rules.example.com",
Version: "v1",
Resource: "replicalimits",
}, nil
}
func validMutatingAdmissionPolicy() *admissionregistration.MutatingAdmissionPolicy {
ignore := admissionregistration.Ignore
return &admissionregistration.MutatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: admissionregistration.MutatingAdmissionPolicySpec{
ParamKind: &admissionregistration.ParamKind{
Kind: "ReplicaLimit",
APIVersion: "rules.example.com/v1",
},
ReinvocationPolicy: admissionregistration.IfNeededReinvocationPolicy,
Mutations: []admissionregistration.Mutation{
{
PatchType: admissionregistration.PatchTypeApplyConfiguration,
ApplyConfiguration: &admissionregistration.ApplyConfiguration{
Expression: `Object{
spec: Object.spec{
replicas: object.spec.replicas % 2 == 0?object.spec.replicas + 1:object.spec.replicas
}
}`,
},
},
},
MatchConstraints: &admissionregistration.MatchResources{
MatchPolicy: func() *admissionregistration.MatchPolicyType {
r := admissionregistration.MatchPolicyType("Exact")
return &r
}(),
ObjectSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
NamespaceSelector: &metav1.LabelSelector{
MatchLabels: map[string]string{"a": "b"},
},
ResourceRules: []admissionregistration.NamedRuleWithOperations{
{
RuleWithOperations: admissionregistration.RuleWithOperations{
Operations: []admissionregistration.OperationType{"CREATE"},
Rule: admissionregistration.Rule{
APIGroups: []string{"a"},
APIVersions: []string{"a"},
Resources: []string{"a"},
},
},
},
},
},
FailurePolicy: &ignore,
},
}
}

View File

@ -0,0 +1,137 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutatingadmissionpolicybinding
import (
"context"
"fmt"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authorization/authorizer"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/kubernetes/pkg/apis/admissionregistration"
rbacregistry "k8s.io/kubernetes/pkg/registry/rbac"
)
func (v *mutatingAdmissionPolicyBindingStrategy) authorizeCreate(ctx context.Context, obj runtime.Object) error {
binding := obj.(*admissionregistration.MutatingAdmissionPolicyBinding)
if binding.Spec.ParamRef == nil {
// no paramRef in new object
return nil
}
return v.authorize(ctx, binding)
}
func (v *mutatingAdmissionPolicyBindingStrategy) authorizeUpdate(ctx context.Context, obj, old runtime.Object) error {
binding := obj.(*admissionregistration.MutatingAdmissionPolicyBinding)
if binding.Spec.ParamRef == nil {
// no paramRef in new object
return nil
}
oldBinding := old.(*admissionregistration.MutatingAdmissionPolicyBinding)
if oldBinding.Spec.ParamRef != nil && *oldBinding.Spec.ParamRef == *binding.Spec.ParamRef && oldBinding.Spec.PolicyName == binding.Spec.PolicyName {
// identical paramRef and policy to old object
return nil
}
return v.authorize(ctx, binding)
}
func (v *mutatingAdmissionPolicyBindingStrategy) authorize(ctx context.Context, binding *admissionregistration.MutatingAdmissionPolicyBinding) error {
if v.resourceResolver == nil {
return fmt.Errorf(`unexpected internal error: resourceResolver is nil`)
}
if v.authorizer == nil || binding.Spec.ParamRef == nil {
return nil
}
// for superuser, skip all checks
if rbacregistry.EscalationAllowed(ctx) {
return nil
}
user, ok := genericapirequest.UserFrom(ctx)
if !ok {
return fmt.Errorf("cannot identify user to authorize read access to paramRef object")
}
// default to requiring permissions on all group/version/resources
resource, apiGroup, apiVersion := "*", "*", "*"
var policyErr, gvParseErr, gvrResolveErr error
var policy *admissionregistration.MutatingAdmissionPolicy
policy, policyErr = v.policyGetter.GetMutatingAdmissionPolicy(ctx, binding.Spec.PolicyName)
if policyErr == nil && policy.Spec.ParamKind != nil {
paramKind := policy.Spec.ParamKind
var gv schema.GroupVersion
gv, gvParseErr = schema.ParseGroupVersion(paramKind.APIVersion)
if gvParseErr == nil {
// we only need to authorize the parsed group/version
apiGroup = gv.Group
apiVersion = gv.Version
var gvr schema.GroupVersionResource
gvr, gvrResolveErr = v.resourceResolver.Resolve(gv.WithKind(paramKind.Kind))
if gvrResolveErr == nil {
// we only need to authorize the resolved resource
resource = gvr.Resource
}
}
}
var attrs authorizer.AttributesRecord
paramRef := binding.Spec.ParamRef
verb := "get"
if len(paramRef.Name) == 0 {
verb = "list"
}
attrs = authorizer.AttributesRecord{
User: user,
Verb: verb,
ResourceRequest: true,
Name: paramRef.Name,
Namespace: paramRef.Namespace, // if empty, no namespace indicates get across all namespaces
APIGroup: apiGroup,
APIVersion: apiVersion,
Resource: resource,
}
d, _, err := v.authorizer.Authorize(ctx, attrs)
if err != nil {
return err
}
if d != authorizer.DecisionAllow {
if policyErr != nil {
return fmt.Errorf(`unable to get policy %s to determine minimum required permissions and user %v does not have "%v" permission for all groups, versions and resources`, binding.Spec.PolicyName, user, verb)
}
if gvParseErr != nil {
return fmt.Errorf(`unable to parse paramKind %v to determine minimum required permissions and user %v does not have "%v" permission for all groups, versions and resources`, policy.Spec.ParamKind, user, verb)
}
if gvrResolveErr != nil {
return fmt.Errorf(`unable to resolve paramKind %v to determine minimum required permissions and user %v does not have "%v" permission for all groups, versions and resources`, policy.Spec.ParamKind, user, verb)
}
return fmt.Errorf(`user %v does not have "%v" permission on the object referenced by paramRef`, verb, user)
}
return nil
}

View File

@ -0,0 +1,233 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutatingadmissionpolicybinding
import (
"context"
"errors"
"fmt"
"strings"
"testing"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authentication/user"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/kubernetes/pkg/apis/admissionregistration"
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
)
func TestAuthorization(t *testing.T) {
for _, tc := range []struct {
name string
userInfo user.Info
auth AuthFunc
policyGetter PolicyGetterFunc
resourceResolver resolver.ResourceResolverFunc
expectErrContains string
}{
{
name: "superuser", // success despite always-denying authorizer
userInfo: &user.DefaultInfo{Groups: []string{user.SystemPrivilegedGroup}},
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
return authorizer.DecisionDeny, "", nil
},
},
{
name: "authorized",
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
if a.GetResource() == "configmaps" {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "", nil
},
policyGetter: func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) {
return &admissionregistration.MutatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"},
Spec: admissionregistration.MutatingAdmissionPolicySpec{
ParamKind: &admissionregistration.ParamKind{Kind: "ConfigMap", APIVersion: "v1"},
},
}, nil
},
resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
return schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "configmaps",
}, nil
},
},
{
name: "denied",
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
if a.GetResource() == "configmaps" {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "", nil
},
policyGetter: func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) {
return &admissionregistration.MutatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"},
Spec: admissionregistration.MutatingAdmissionPolicySpec{
ParamKind: &admissionregistration.ParamKind{Kind: "Params", APIVersion: "foo.example.com/v1"},
},
}, nil
},
resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
return schema.GroupVersionResource{
Group: "foo.example.com",
Version: "v1",
Resource: "params",
}, nil
},
expectErrContains: "permission on the object referenced by paramRef",
},
{
name: "unable to parse paramRef",
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
if a.GetResource() == "configmaps" {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "", nil
},
policyGetter: func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) {
return &admissionregistration.MutatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"},
Spec: admissionregistration.MutatingAdmissionPolicySpec{
ParamKind: &admissionregistration.ParamKind{Kind: "Params", APIVersion: "foo.example.com/v1"},
},
}, nil
},
resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
return schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "configmaps",
}, nil
},
expectErrContains: "unable to parse paramKind &{foo.example.com/v1 Params} to determine minimum required permissions",
},
{
name: "unable to resolve param",
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
if a.GetResource() == "configmaps" {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "", nil
},
policyGetter: func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) {
return &admissionregistration.MutatingAdmissionPolicy{
ObjectMeta: metav1.ObjectMeta{Name: "replicalimit-policy.example.com"},
Spec: admissionregistration.MutatingAdmissionPolicySpec{
ParamKind: &admissionregistration.ParamKind{Kind: "Params", APIVersion: "foo.example.com/v1"},
},
}, nil
},
resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
return schema.GroupVersionResource{}, &meta.NoKindMatchError{GroupKind: gvk.GroupKind(), SearchedVersions: []string{gvk.Version}}
},
expectErrContains: "unable to resolve paramKind &{foo.example.com/v1 Params} to determine minimum required permissions",
},
{
name: "unable to get policy",
userInfo: &user.DefaultInfo{Groups: []string{user.AllAuthenticated}},
auth: func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
if a.GetResource() == "configmaps" {
return authorizer.DecisionAllow, "", nil
}
return authorizer.DecisionDeny, "", nil
},
policyGetter: func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) {
return nil, fmt.Errorf("no such policy")
},
resourceResolver: func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
return schema.GroupVersionResource{
Group: "",
Version: "v1",
Resource: "configmaps",
}, nil
},
expectErrContains: "unable to get policy replicalimit-policy.example.com to determine minimum required permissions",
},
} {
t.Run(tc.name, func(t *testing.T) {
strategy := NewStrategy(tc.auth, tc.policyGetter, tc.resourceResolver)
t.Run("create", func(t *testing.T) {
ctx := request.WithUser(context.Background(), tc.userInfo)
for _, obj := range validPolicyBindings() {
errs := strategy.Validate(ctx, obj)
if len(errs) > 0 && !strings.Contains(errors.Join(errs.ToAggregate().Errors()...).Error(), tc.expectErrContains) {
t.Errorf("expected error to contain: %v but got error: %v", tc.expectErrContains, errs)
}
}
})
t.Run("update", func(t *testing.T) {
ctx := request.WithUser(context.Background(), tc.userInfo)
for _, obj := range validPolicyBindings() {
objWithChangedParamRef := obj.DeepCopy()
if pr := objWithChangedParamRef.Spec.ParamRef; pr != nil {
if len(pr.Name) > 0 {
pr.Name = "changed"
}
if pr.Selector != nil {
pr.Selector = &metav1.LabelSelector{
MatchLabels: map[string]string{
"changed": "value",
},
}
}
if len(pr.Namespace) > 0 {
pr.Namespace = "othernamespace"
}
if pr.ParameterNotFoundAction == nil || *pr.ParameterNotFoundAction == admissionregistration.AllowAction {
v := admissionregistration.DenyAction
pr.ParameterNotFoundAction = &v
} else {
v := admissionregistration.AllowAction
pr.ParameterNotFoundAction = &v
}
}
errs := strategy.ValidateUpdate(ctx, obj, objWithChangedParamRef)
if len(errs) > 0 && !strings.Contains(errors.Join(errs.ToAggregate().Errors()...).Error(), tc.expectErrContains) {
t.Errorf("expected error to contain: %v but got error: %v", tc.expectErrContains, errs)
}
}
})
})
}
}
type AuthFunc func(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error)
func (f AuthFunc) Authorize(ctx context.Context, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
return f(ctx, a)
}
type PolicyGetterFunc func(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error)
func (f PolicyGetterFunc) GetMutatingAdmissionPolicy(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) {
return f(ctx, name)
}

View File

@ -0,0 +1,17 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutatingadmissionpolicybinding // import "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicybinding"

View File

@ -0,0 +1,94 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package storage
import (
"context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/generic"
genericregistry "k8s.io/apiserver/pkg/registry/generic/registry"
"k8s.io/apiserver/pkg/registry/rest"
"k8s.io/kubernetes/pkg/apis/admissionregistration"
"k8s.io/kubernetes/pkg/printers"
printersinternal "k8s.io/kubernetes/pkg/printers/internalversion"
printerstorage "k8s.io/kubernetes/pkg/printers/storage"
mutatingadmissionpolicybinding "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicybinding"
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
)
// REST implements a RESTStorage for policyBinding against etcd
type REST struct {
*genericregistry.Store
}
var groupResource = admissionregistration.Resource("mutatingadmissionpolicybindings")
// NewREST returns a RESTStorage object that will work against policyBinding.
func NewREST(optsGetter generic.RESTOptionsGetter, authorizer authorizer.Authorizer, policyGetter PolicyGetter, resourceResolver resolver.ResourceResolver) (*REST, error) {
r := &REST{}
strategy := mutatingadmissionpolicybinding.NewStrategy(authorizer, policyGetter, resourceResolver)
store := &genericregistry.Store{
NewFunc: func() runtime.Object { return &admissionregistration.MutatingAdmissionPolicyBinding{} },
NewListFunc: func() runtime.Object { return &admissionregistration.MutatingAdmissionPolicyBindingList{} },
ObjectNameFunc: func(obj runtime.Object) (string, error) {
return obj.(*admissionregistration.MutatingAdmissionPolicyBinding).Name, nil
},
DefaultQualifiedResource: groupResource,
SingularQualifiedResource: admissionregistration.Resource("mutatingadmissionpolicybinding"),
CreateStrategy: strategy,
UpdateStrategy: strategy,
DeleteStrategy: strategy,
TableConvertor: printerstorage.TableConvertor{TableGenerator: printers.NewTableGenerator().With(printersinternal.AddHandlers)},
}
options := &generic.StoreOptions{RESTOptions: optsGetter}
if err := store.CompleteWithOptions(options); err != nil {
return nil, err
}
r.Store = store
return r, nil
}
// Implement CategoriesProvider
var _ rest.CategoriesProvider = &REST{}
// Categories implements the CategoriesProvider interface. Returns a list of categories a resource is part of.
func (r *REST) Categories() []string {
return []string{"api-extensions"}
}
type PolicyGetter interface {
// GetMutatingAdmissionPolicy returns a GetMutatingAdmissionPolicy
// by its name. There is no namespace because it is cluster-scoped.
GetMutatingAdmissionPolicy(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error)
}
type DefaultPolicyGetter struct {
Getter rest.Getter
}
func (g *DefaultPolicyGetter) GetMutatingAdmissionPolicy(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error) {
p, err := g.Getter.Get(ctx, name, &metav1.GetOptions{})
if err != nil {
return nil, err
}
return p.(*admissionregistration.MutatingAdmissionPolicy), err
}

View File

@ -0,0 +1,260 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package storage
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/fields"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/registry/generic"
genericregistrytest "k8s.io/apiserver/pkg/registry/generic/testing"
etcd3testing "k8s.io/apiserver/pkg/storage/etcd3/testing"
"k8s.io/kubernetes/pkg/apis/admissionregistration"
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
"k8s.io/kubernetes/pkg/registry/registrytest"
// Ensure that admissionregistration package is initialized.
_ "k8s.io/kubernetes/pkg/apis/admissionregistration/install"
)
func TestCreate(t *testing.T) {
for _, configuration := range validPolicyBindings() {
t.Run(configuration.Name, func(t *testing.T) {
storage, server := newInsecureStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestCreate(
// valid
configuration,
// invalid
newPolicyBinding(""),
)
})
}
}
func TestUpdate(t *testing.T) {
for _, b := range validPolicyBindings() {
storage, server := newInsecureStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
t.Run(b.Name, func(t *testing.T) {
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestUpdate(
// valid
b,
// updateFunc
func(obj runtime.Object) runtime.Object {
object := obj.(*admissionregistration.MutatingAdmissionPolicyBinding)
object.Labels = map[string]string{"c": "d"}
return object
},
// invalid updateFunc
func(obj runtime.Object) runtime.Object {
object := obj.(*admissionregistration.MutatingAdmissionPolicyBinding)
object.Name = ""
return object
},
)
})
}
}
func TestGet(t *testing.T) {
for _, b := range validPolicyBindings() {
t.Run(b.Name, func(t *testing.T) {
storage, server := newInsecureStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestGet(b)
})
}
}
func TestList(t *testing.T) {
for _, b := range validPolicyBindings() {
t.Run(b.Name, func(t *testing.T) {
storage, server := newInsecureStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestList(b)
})
}
}
func TestDelete(t *testing.T) {
for _, b := range validPolicyBindings() {
t.Run(b.Name, func(t *testing.T) {
storage, server := newInsecureStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestDelete(b)
})
}
}
func TestWatch(t *testing.T) {
for _, b := range validPolicyBindings() {
t.Run(b.Name, func(t *testing.T) {
storage, server := newInsecureStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
test := genericregistrytest.New(t, storage.Store).ClusterScope()
test.TestWatch(
b,
[]labels.Set{},
[]labels.Set{
{"hoo": "bar"},
},
[]fields.Set{
{"metadata.name": b.Name},
},
[]fields.Set{
{"metadata.name": "nomatch"},
},
)
})
}
}
func validPolicyBindings() []*admissionregistration.MutatingAdmissionPolicyBinding {
denyAction := admissionregistration.DenyAction
return []*admissionregistration.MutatingAdmissionPolicyBinding{
{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{
PolicyName: "replicalimit-policy.example.com",
ParamRef: &admissionregistration.ParamRef{
Name: "replica-limit-test.example.com",
ParameterNotFoundAction: &denyAction,
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "foo-clusterwide",
},
Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{
PolicyName: "replicalimit-policy.example.com",
ParamRef: &admissionregistration.ParamRef{
Name: "replica-limit-test.example.com",
Namespace: "default",
ParameterNotFoundAction: &denyAction,
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "foo-selector",
},
Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{
PolicyName: "replicalimit-policy.example.com",
ParamRef: &admissionregistration.ParamRef{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"label": "value",
},
},
ParameterNotFoundAction: &denyAction,
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "foo-selector-clusterwide",
},
Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{
PolicyName: "replicalimit-policy.example.com",
ParamRef: &admissionregistration.ParamRef{
Namespace: "mynamespace",
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"label": "value",
},
},
ParameterNotFoundAction: &denyAction,
},
},
},
}
}
func newPolicyBinding(name string) *admissionregistration.MutatingAdmissionPolicyBinding {
return &admissionregistration.MutatingAdmissionPolicyBinding{
ObjectMeta: metav1.ObjectMeta{
Name: name,
Labels: map[string]string{"foo": "bar"},
},
Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{
PolicyName: "replicalimit-policy.example.com",
ParamRef: &admissionregistration.ParamRef{
Name: "param-test",
Namespace: "default",
},
MatchResources: &admissionregistration.MatchResources{},
},
}
}
func newInsecureStorage(t *testing.T) (*REST, *etcd3testing.EtcdTestServer) {
return newStorage(t, nil, nil, replicaLimitsResolver)
}
func newStorage(t *testing.T, authorizer authorizer.Authorizer, policyGetter PolicyGetter, resourceResolver resolver.ResourceResolver) (*REST, *etcd3testing.EtcdTestServer) {
etcdStorage, server := registrytest.NewEtcdStorageForResource(t, admissionregistration.Resource("mutatingadmissionpolicybindings"))
restOptions := generic.RESTOptions{
StorageConfig: etcdStorage,
Decorator: generic.UndecoratedStorage,
DeleteCollectionWorkers: 1,
ResourcePrefix: "mutatingadmissionpolicybindings"}
storage, err := NewREST(restOptions, authorizer, policyGetter, resourceResolver)
if err != nil {
t.Fatalf("unexpected error from REST storage: %v", err)
}
return storage, server
}
func TestCategories(t *testing.T) {
storage, server := newInsecureStorage(t)
defer server.Terminate(t)
defer storage.Store.DestroyFunc()
expected := []string{"api-extensions"}
registrytest.AssertCategories(t, storage, expected)
}
var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
return schema.GroupVersionResource{
Group: "rules.example.com",
Version: "v1",
Resource: "replicalimits",
}, nil
}

View File

@ -0,0 +1,130 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutatingadmissionpolicybinding
import (
"context"
apiequality "k8s.io/apimachinery/pkg/api/equality"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apiserver/pkg/authorization/authorizer"
"k8s.io/apiserver/pkg/storage/names"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/admissionregistration"
"k8s.io/kubernetes/pkg/apis/admissionregistration/validation"
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
)
// MutatingAdmissionPolicyBindingStrategy implements verification logic for MutatingAdmissionPolicyBinding.
type mutatingAdmissionPolicyBindingStrategy struct {
runtime.ObjectTyper
names.NameGenerator
authorizer authorizer.Authorizer
policyGetter PolicyGetter
resourceResolver resolver.ResourceResolver
}
type PolicyGetter interface {
// GetMutatingAdmissionPolicy returns a GetMutatingAdmissionPolicy
// by its name. There is no namespace because it is cluster-scoped.
GetMutatingAdmissionPolicy(ctx context.Context, name string) (*admissionregistration.MutatingAdmissionPolicy, error)
}
// NewStrategy is the default logic that applies when creating and updating MutatingAdmissionPolicyBinding objects.
func NewStrategy(authorizer authorizer.Authorizer, policyGetter PolicyGetter, resourceResolver resolver.ResourceResolver) *mutatingAdmissionPolicyBindingStrategy {
return &mutatingAdmissionPolicyBindingStrategy{
ObjectTyper: legacyscheme.Scheme,
NameGenerator: names.SimpleNameGenerator,
authorizer: authorizer,
policyGetter: policyGetter,
resourceResolver: resourceResolver,
}
}
// NamespaceScoped returns false because MutatingAdmissionPolicyBinding is cluster-scoped resource.
func (v *mutatingAdmissionPolicyBindingStrategy) NamespaceScoped() bool {
return false
}
// PrepareForCreate clears the status of an MutatingAdmissionPolicyBinding before creation.
func (v *mutatingAdmissionPolicyBindingStrategy) PrepareForCreate(ctx context.Context, obj runtime.Object) {
ic := obj.(*admissionregistration.MutatingAdmissionPolicyBinding)
ic.Generation = 1
}
// PrepareForUpdate clears fields that are not allowed to be set by end users on update.
func (v *mutatingAdmissionPolicyBindingStrategy) PrepareForUpdate(ctx context.Context, obj, old runtime.Object) {
newIC := obj.(*admissionregistration.MutatingAdmissionPolicyBinding)
oldIC := old.(*admissionregistration.MutatingAdmissionPolicyBinding)
// Any changes to the spec increment the generation number, any changes to the
// status should reflect the generation number of the corresponding object.
// See metav1.ObjectMeta description for more information on Generation.
if !apiequality.Semantic.DeepEqual(oldIC.Spec, newIC.Spec) {
newIC.Generation = oldIC.Generation + 1
}
}
// Validate validates a new MutatingAdmissionPolicyBinding.
func (v *mutatingAdmissionPolicyBindingStrategy) Validate(ctx context.Context, obj runtime.Object) field.ErrorList {
errs := validation.ValidateMutatingAdmissionPolicyBinding(obj.(*admissionregistration.MutatingAdmissionPolicyBinding))
if len(errs) == 0 {
// if the object is well-formed, also authorize the paramRef
if err := v.authorizeCreate(ctx, obj); err != nil {
errs = append(errs, field.Forbidden(field.NewPath("spec", "paramRef"), err.Error()))
}
}
return errs
}
// WarningsOnCreate returns warnings for the creation of the given object.
func (v *mutatingAdmissionPolicyBindingStrategy) WarningsOnCreate(ctx context.Context, obj runtime.Object) []string {
return nil
}
// Canonicalize normalizes the object after validation.
func (v *mutatingAdmissionPolicyBindingStrategy) Canonicalize(obj runtime.Object) {
}
// AllowCreateOnUpdate is false for MutatingAdmissionPolicyBinding; this means you may not create one with a PUT request.
func (v *mutatingAdmissionPolicyBindingStrategy) AllowCreateOnUpdate() bool {
return false
}
// ValidateUpdate is the default update validation for an end user.
func (v *mutatingAdmissionPolicyBindingStrategy) ValidateUpdate(ctx context.Context, obj, old runtime.Object) field.ErrorList {
errs := validation.ValidateMutatingAdmissionPolicyBindingUpdate(obj.(*admissionregistration.MutatingAdmissionPolicyBinding), old.(*admissionregistration.MutatingAdmissionPolicyBinding))
if len(errs) == 0 {
// if the object is well-formed, also authorize the paramRef
if err := v.authorizeUpdate(ctx, obj, old); err != nil {
errs = append(errs, field.Forbidden(field.NewPath("spec", "paramRef"), err.Error()))
}
}
return errs
}
// WarningsOnUpdate returns warnings for the given update.
func (v *mutatingAdmissionPolicyBindingStrategy) WarningsOnUpdate(ctx context.Context, obj, old runtime.Object) []string {
return nil
}
// AllowUnconditionalUpdate is the default update policy for MutatingAdmissionPolicyBinding objects. Status update should
// only be allowed if version match.
func (v *mutatingAdmissionPolicyBindingStrategy) AllowUnconditionalUpdate() bool {
return false
}

View File

@ -0,0 +1,127 @@
/*
Copyright 2022 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package mutatingadmissionpolicybinding
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
"k8s.io/kubernetes/pkg/apis/admissionregistration"
)
func TestPolicyBindingStrategy(t *testing.T) {
strategy := NewStrategy(nil, nil, replicaLimitsResolver)
ctx := genericapirequest.NewDefaultContext()
if strategy.NamespaceScoped() {
t.Error("PolicyBinding strategy must be cluster scoped")
}
if strategy.AllowCreateOnUpdate() {
t.Errorf("PolicyBinding should not allow create on update")
}
for _, configuration := range validPolicyBindings() {
strategy.PrepareForCreate(ctx, configuration)
errs := strategy.Validate(ctx, configuration)
if len(errs) != 0 {
t.Errorf("Unexpected error validating %v", errs)
}
invalidConfiguration := &admissionregistration.MutatingAdmissionPolicyBinding{
ObjectMeta: metav1.ObjectMeta{Name: ""},
}
strategy.PrepareForUpdate(ctx, invalidConfiguration, configuration)
errs = strategy.ValidateUpdate(ctx, invalidConfiguration, configuration)
if len(errs) == 0 {
t.Errorf("Expected a validation error")
}
}
}
var replicaLimitsResolver resolver.ResourceResolverFunc = func(gvk schema.GroupVersionKind) (schema.GroupVersionResource, error) {
return schema.GroupVersionResource{
Group: "rules.example.com",
Version: "v1",
Resource: "replicalimits",
}, nil
}
func validPolicyBindings() []*admissionregistration.MutatingAdmissionPolicyBinding {
denyAction := admissionregistration.DenyAction
return []*admissionregistration.MutatingAdmissionPolicyBinding{
{
ObjectMeta: metav1.ObjectMeta{
Name: "foo",
},
Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{
PolicyName: "replicalimit-policy.example.com",
ParamRef: &admissionregistration.ParamRef{
Name: "replica-limit-test.example.com",
ParameterNotFoundAction: &denyAction,
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "foo-clusterwide",
},
Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{
PolicyName: "replicalimit-policy.example.com",
ParamRef: &admissionregistration.ParamRef{
Name: "replica-limit-test.example.com",
Namespace: "default",
ParameterNotFoundAction: &denyAction,
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "foo-selector",
},
Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{
PolicyName: "replicalimit-policy.example.com",
ParamRef: &admissionregistration.ParamRef{
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"label": "value",
},
},
ParameterNotFoundAction: &denyAction,
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "foo-selector-clusterwide",
},
Spec: admissionregistration.MutatingAdmissionPolicyBindingSpec{
PolicyName: "replicalimit-policy.example.com",
ParamRef: &admissionregistration.ParamRef{
Namespace: "mynamespace",
Selector: &metav1.LabelSelector{
MatchLabels: map[string]string{
"label": "value",
},
},
ParameterNotFoundAction: &denyAction,
},
},
},
}
}

View File

@ -28,6 +28,8 @@ import (
"k8s.io/client-go/discovery"
"k8s.io/kubernetes/pkg/api/legacyscheme"
"k8s.io/kubernetes/pkg/apis/admissionregistration"
mutatingadmissionpolicystorage "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicy/storage"
mutationpolicybindingstorage "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingadmissionpolicybinding/storage"
mutatingwebhookconfigurationstorage "k8s.io/kubernetes/pkg/registry/admissionregistration/mutatingwebhookconfiguration/storage"
"k8s.io/kubernetes/pkg/registry/admissionregistration/resolver"
validatingadmissionpolicystorage "k8s.io/kubernetes/pkg/registry/admissionregistration/validatingadmissionpolicy/storage"
@ -148,6 +150,25 @@ func (p RESTStorageProvider) v1alpha1Storage(apiResourceConfigSource serverstora
storage[resource] = policyBindingStorage
}
// mutatingadmissionpolicies
if resource := "mutatingadmissionpolicies"; apiResourceConfigSource.ResourceEnabled(admissionregistrationv1alpha1.SchemeGroupVersion.WithResource(resource)) {
policyStorage, err := mutatingadmissionpolicystorage.NewREST(restOptionsGetter, p.Authorizer, r)
if err != nil {
return storage, err
}
policyGetter = policyStorage
storage[resource] = policyStorage
}
// mutatingadmissionpolicybindings
if resource := "mutatingadmissionpolicybindings"; apiResourceConfigSource.ResourceEnabled(admissionregistrationv1alpha1.SchemeGroupVersion.WithResource(resource)) {
mutationpolicybindingstorage, err := mutationpolicybindingstorage.NewREST(restOptionsGetter, p.Authorizer, &mutationpolicybindingstorage.DefaultPolicyGetter{Getter: policyGetter}, r)
if err != nil {
return storage, err
}
storage[resource] = mutationpolicybindingstorage
}
return storage, nil
}

View File

@ -50,6 +50,10 @@ func addKnownTypes(scheme *runtime.Scheme) error {
&ValidatingAdmissionPolicyList{},
&ValidatingAdmissionPolicyBinding{},
&ValidatingAdmissionPolicyBindingList{},
&MutatingAdmissionPolicy{},
&MutatingAdmissionPolicyList{},
&MutatingAdmissionPolicyBinding{},
&MutatingAdmissionPolicyBindingList{},
)
metav1.AddToGroupVersion(scheme, SchemeGroupVersion)
return nil

View File

@ -663,3 +663,346 @@ const (
Delete OperationType = v1.Delete
Connect OperationType = v1.Connect
)
// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.32
// MutatingAdmissionPolicy describes the definition of an admission mutation policy that mutates the object coming into admission chain.
type MutatingAdmissionPolicy struct {
metav1.TypeMeta `json:",inline"`
// Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata.
// +optional
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// Specification of the desired behavior of the MutatingAdmissionPolicy.
Spec MutatingAdmissionPolicySpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.32
// MutatingAdmissionPolicyList is a list of MutatingAdmissionPolicy.
type MutatingAdmissionPolicyList struct {
metav1.TypeMeta `json:",inline"`
// Standard list metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
// +optional
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// List of ValidatingAdmissionPolicy.
Items []MutatingAdmissionPolicy `json:"items" protobuf:"bytes,2,rep,name=items"`
}
// MutatingAdmissionPolicySpec is the specification of the desired behavior of the admission policy.
type MutatingAdmissionPolicySpec struct {
// paramKind specifies the kind of resources used to parameterize this policy.
// If absent, there are no parameters for this policy and the param CEL variable will not be provided to validation expressions.
// If paramKind refers to a non-existent kind, this policy definition is mis-configured and the FailurePolicy is applied.
// If paramKind is specified but paramRef is unset in MutatingAdmissionPolicyBinding, the params variable will be null.
// +optional
ParamKind *ParamKind `json:"paramKind,omitempty" protobuf:"bytes,1,rep,name=paramKind"`
// matchConstraints specifies what resources this policy is designed to validate.
// The MutatingAdmissionPolicy cares about a request if it matches _all_ Constraints.
// However, in order to prevent clusters from being put into an unstable state that cannot be recovered from via the API
// MutatingAdmissionPolicy cannot match MutatingAdmissionPolicy and MutatingAdmissionPolicyBinding.
// The CREATE, UPDATE and CONNECT operations are allowed. The DELETE operation may not be matched.
// '*' matches CREATE, UPDATE and CONNECT.
// Required.
MatchConstraints *MatchResources `json:"matchConstraints,omitempty" protobuf:"bytes,2,rep,name=matchConstraints"`
// variables contain definitions of variables that can be used in composition of other expressions.
// Each variable is defined as a named CEL expression.
// The variables defined here will be available under `variables` in other expressions of the policy
// except matchConditions because matchConditions are evaluated before the rest of the policy.
//
// The expression of a variable can refer to other variables defined earlier in the list but not those after.
// Thus, variables must be sorted by the order of first appearance and acyclic.
// +listType=atomic
// +optional
Variables []Variable `json:"variables,omitempty" protobuf:"bytes,3,rep,name=variables"`
// mutations contain operations to perform on matching objects.
// mutations may not be empty; a minimum of one mutation is required.
// mutations are evaluated in order, and are reinvoked according to
// the reinvocationPolicy.
// The mutations of a policy are invoked for each binding of this policy
// and reinvocation of mutations occurs on a per binding basis.
//
// +listType=atomic
// +optional
Mutations []Mutation `json:"mutations,omitempty" protobuf:"bytes,4,rep,name=mutations"`
// failurePolicy defines how to handle failures for the admission policy. Failures can
// 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 paramKind refers to a non-existent Kind.
// A binding is invalid if paramRef.name refers to a non-existent resource.
//
// failurePolicy does not define how validations that evaluate to false are handled.
//
// Allowed values are Ignore or Fail. Defaults to Fail.
// +optional
FailurePolicy *FailurePolicyType `json:"failurePolicy,omitempty" protobuf:"bytes,5,opt,name=failurePolicy,casttype=FailurePolicyType"`
// matchConditions is a list of conditions that must be met for a request to be validated.
// Match conditions filter requests that have already been matched by the matchConstraints.
// An empty list of matchConditions matches all requests.
// There are a maximum of 64 match conditions allowed.
//
// If a parameter object is provided, it can be accessed via the `params` handle in the same
// manner as validation expressions.
//
// The exact matching logic is (in order):
// 1. If ANY matchCondition evaluates to FALSE, the policy is skipped.
// 2. If ALL matchConditions evaluate to TRUE, the policy is evaluated.
// 3. If any matchCondition evaluates to an error (but none are FALSE):
// - If failurePolicy=Fail, reject the request
// - If failurePolicy=Ignore, the policy is skipped
//
// +patchMergeKey=name
// +patchStrategy=merge
// +listType=map
// +listMapKey=name
// +optional
MatchConditions []MatchCondition `json:"matchConditions,omitempty" patchStrategy:"merge" patchMergeKey:"name" protobuf:"bytes,6,rep,name=matchConditions"`
// reinvocationPolicy indicates whether mutations may be called multiple times per MutatingAdmissionPolicyBinding
// as part of a single admission evaluation.
// Allowed values are "Never" and "IfNeeded".
//
// Never: These mutations will not be called more than once per binding in a single admission evaluation.
//
// IfNeeded: These mutations may be invoked more than once per binding for a single admission request and there is no guarantee of
// order with respect to other admission plugins, admission webhooks, bindings of this policy and admission policies. Mutations are only
// reinvoked when mutations change the object after this mutation is invoked.
// Required.
ReinvocationPolicy ReinvocationPolicyType `json:"reinvocationPolicy,omitempty" protobuf:"bytes,7,opt,name=reinvocationPolicy,casttype=ReinvocationPolicyType"`
}
// Mutation specifies the CEL expression which is used to apply the Mutation.
type Mutation struct {
// patchType indicates the patch strategy used.
// Allowed values are "ApplyConfiguration" and "JSONPatch".
// Required.
//
// +unionDiscriminator
PatchType PatchType `json:"patchType" protobuf:"bytes,2,opt,name=patchType,casttype=PatchType"`
// applyConfiguration defines the desired configuration values of an object.
// The configuration is applied to the admission object using
// [structured merge diff](https://github.com/kubernetes-sigs/structured-merge-diff).
// A CEL expression is used to create apply configuration.
ApplyConfiguration *ApplyConfiguration `json:"applyConfiguration,omitempty" protobuf:"bytes,3,opt,name=applyConfiguration"`
// jsonPatch defines a [JSON patch](https://jsonpatch.com/) operation to perform a mutation to the object.
// A CEL expression is used to create the JSON patch.
JSONPatch *JSONPatch `json:"jsonPatch,omitempty" protobuf:"bytes,4,opt,name=jsonPatch"`
}
// PatchType specifies the type of patch operation for a mutation.
// +enum
type PatchType string
const (
// ApplyConfiguration indicates that the mutation is using apply configuration to mutate the object.
PatchTypeApplyConfiguration PatchType = "ApplyConfiguration"
// JSONPatch indicates that the object is mutated through JSON Patch.
PatchTypeJSONPatch PatchType = "JSONPatch"
)
// ApplyConfiguration defines the desired configuration values of an object.
type ApplyConfiguration struct {
// expression will be evaluated by CEL to create an apply configuration.
// ref: https://github.com/google/cel-spec
//
// Apply configurations are declared in CEL using object initialization. For example, this CEL expression
// returns an apply configuration to set a single field:
//
// Object{
// spec: Object.spec{
// serviceAccountName: "example"
// }
// }
//
// Apply configurations may not modify atomic structs, maps or arrays due to the risk of accidental deletion of
// values not included in the apply configuration.
//
// CEL expressions have access to the object types needed to create apply configurations:
//
// - 'Object' - CEL type of the resource object.
// - 'Object.<fieldName>' - CEL type of object field (such as 'Object.spec')
// - 'Object.<fieldName1>.<fieldName2>...<fieldNameN>` - CEL type of nested field (such as 'Object.spec.containers')
//
// CEL expressions have access to the contents of the API request, 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.
// - 'oldObject' - The existing object. The value is null for CREATE requests.
// - '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.
// - 'namespaceObject' - The namespace object that the incoming object belongs to. The value is null for cluster-scoped resources.
// - 'variables' - Map of composited variables, from its name to its lazily evaluated value.
// For example, a variable named 'foo' can be accessed as 'variables.foo'.
// - '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
// - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the
// request resource.
//
// The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the
// object. No other metadata properties are accessible.
//
// Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible.
// Required.
Expression string `json:"expression,omitempty" protobuf:"bytes,1,opt,name=expression"`
}
// JSONPatch defines a JSON Patch.
type JSONPatch struct {
// expression will be evaluated by CEL to create a [JSON patch](https://jsonpatch.com/).
// ref: https://github.com/google/cel-spec
//
// expression must return an array of JSONPatch values.
//
// For example, this CEL expression returns a JSON patch to conditionally modify a value:
//
// [
// JSONPatch{op: "test", path: "/spec/example", value: "Red"},
// JSONPatch{op: "replace", path: "/spec/example", value: "Green"}
// ]
//
// To define an object for the patch value, use Object types. For example:
//
// [
// JSONPatch{
// op: "add",
// path: "/spec/selector",
// value: Object.spec.selector{matchLabels: {"environment": "test"}}
// }
// ]
//
// To use strings containing '/' and '~' as JSONPatch path keys, use "jsonpatch.escapeKey". For example:
//
// [
// JSONPatch{
// op: "add",
// path: "/metadata/labels/" + jsonpatch.escapeKey("example.com/environment"),
// value: "test"
// },
// ]
//
// CEL expressions have access to the types needed to create JSON patches and objects:
//
// - 'JSONPatch' - CEL type of JSON Patch operations. JSONPatch has the fields 'op', 'from', 'path' and 'value'.
// See [JSON patch](https://jsonpatch.com/) for more details. The 'value' field may be set to any of: string,
// integer, array, map or object. If set, the 'path' and 'from' fields must be set to a
// [JSON pointer](https://datatracker.ietf.org/doc/html/rfc6901/) string, where the 'jsonpatch.escapeKey()' CEL
// function may be used to escape path keys containing '/' and '~'.
// - 'Object' - CEL type of the resource object.
// - 'Object.<fieldName>' - CEL type of object field (such as 'Object.spec')
// - 'Object.<fieldName1>.<fieldName2>...<fieldNameN>` - CEL type of nested field (such as 'Object.spec.containers')
//
// CEL expressions have access to the contents of the API request, 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.
// - 'oldObject' - The existing object. The value is null for CREATE requests.
// - '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.
// - 'namespaceObject' - The namespace object that the incoming object belongs to. The value is null for cluster-scoped resources.
// - 'variables' - Map of composited variables, from its name to its lazily evaluated value.
// For example, a variable named 'foo' can be accessed as 'variables.foo'.
// - '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
// - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the
// request resource.
//
// CEL expressions have access to [Kubernetes CEL function libraries](https://kubernetes.io/docs/reference/using-api/cel/#cel-options-language-features-and-libraries)
// as well as:
//
// - 'jsonpatch.escapeKey' - Performs JSONPatch key escaping. '~' and '/' are escaped as '~0' and `~1' respectively).
//
//
// Only property names of the form `[a-zA-Z_.-/][a-zA-Z0-9_.-/]*` are accessible.
// Required.
Expression string `json:"expression,omitempty" protobuf:"bytes,1,opt,name=expression"`
}
// ReinvocationPolicyType specifies what type of policy the admission mutation uses.
// +enum
type ReinvocationPolicyType = v1.ReinvocationPolicyType
const (
// NeverReinvocationPolicy indicates that the mutation must not be called more than once in a
// single admission evaluation.
NeverReinvocationPolicy ReinvocationPolicyType = v1.NeverReinvocationPolicy
// IfNeededReinvocationPolicy indicates that the mutation may be called at least one
// additional time as part of the admission evaluation if the object being admitted is
// modified by other admission plugins after the initial mutation call.
IfNeededReinvocationPolicy ReinvocationPolicyType = v1.IfNeededReinvocationPolicy
)
// +genclient
// +genclient:nonNamespaced
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.32
// MutatingAdmissionPolicyBinding binds the MutatingAdmissionPolicy with parametrized resources.
// MutatingAdmissionPolicyBinding and the optional parameter resource together define how cluster administrators
// configure policies for clusters.
//
// For a given admission request, each binding will cause its policy to be
// evaluated N times, where N is 1 for policies/bindings that don't use
// params, otherwise N is the number of parameters selected by the binding.
// Each evaluation is constrained by a [runtime cost budget](https://kubernetes.io/docs/reference/using-api/cel/#runtime-cost-budget).
//
// Adding/removing policies, bindings, or params can not affect whether a
// given (policy, binding, param) combination is within its own CEL budget.
type MutatingAdmissionPolicyBinding struct {
metav1.TypeMeta `json:",inline"`
// Standard object metadata; More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#metadata.
// +optional
metav1.ObjectMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// Specification of the desired behavior of the MutatingAdmissionPolicyBinding.
Spec MutatingAdmissionPolicyBindingSpec `json:"spec,omitempty" protobuf:"bytes,2,opt,name=spec"`
}
// +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object
// +k8s:prerelease-lifecycle-gen:introduced=1.32
// MutatingAdmissionPolicyBindingList is a list of MutatingAdmissionPolicyBinding.
type MutatingAdmissionPolicyBindingList struct {
metav1.TypeMeta `json:",inline"`
// Standard list metadata.
// More info: https://git.k8s.io/community/contributors/devel/sig-architecture/api-conventions.md#types-kinds
// +optional
metav1.ListMeta `json:"metadata,omitempty" protobuf:"bytes,1,opt,name=metadata"`
// List of PolicyBinding.
Items []MutatingAdmissionPolicyBinding `json:"items" protobuf:"bytes,2,rep,name=items"`
}
// MutatingAdmissionPolicyBindingSpec is the specification of the MutatingAdmissionPolicyBinding.
type MutatingAdmissionPolicyBindingSpec struct {
// policyName references a MutatingAdmissionPolicy name which the MutatingAdmissionPolicyBinding binds to.
// If the referenced resource does not exist, this binding is considered invalid and will be ignored
// Required.
PolicyName string `json:"policyName,omitempty" protobuf:"bytes,1,rep,name=policyName"`
// paramRef specifies the parameter resource used to configure the admission control policy.
// It should point to a resource of the type specified in spec.ParamKind of the bound MutatingAdmissionPolicy.
// If the policy specifies a ParamKind and the resource referred to by ParamRef does not exist, this binding is considered mis-configured and the FailurePolicy of the MutatingAdmissionPolicy applied.
// If the policy does not specify a ParamKind then this field is ignored, and the rules are evaluated without a param.
// +optional
ParamRef *ParamRef `json:"paramRef,omitempty" protobuf:"bytes,2,rep,name=paramRef"`
// matchResources limits what resources match this binding and may be mutated by it.
// Note that if matchResources matches a resource, the resource must also match a policy's matchConstraints and
// matchConditions before the resource may be mutated.
// When matchResources is unset, it does not constrain resource matching, and only the policy's matchConstraints
// and matchConditions must match for the resource to be mutated.
// Additionally, matchResources.resourceRules are optional and do not constraint matching when unset.
// Note that this is differs from MutatingAdmissionPolicy matchConstraints, where resourceRules are required.
// The CREATE, UPDATE and CONNECT operations are allowed. The DELETE operation may not be matched.
// '*' matches CREATE, UPDATE and CONNECT.
// +optional
MatchResources *MatchResources `json:"matchResources,omitempty" protobuf:"bytes,3,rep,name=matchResources"`
}

View File

@ -121,7 +121,7 @@ func (r *Matcher) resource() bool {
func IsExemptAdmissionConfigurationResource(attr admission.Attributes) bool {
gvk := attr.GetKind()
if gvk.Group == "admissionregistration.k8s.io" {
if gvk.Kind == "ValidatingWebhookConfiguration" || gvk.Kind == "MutatingWebhookConfiguration" || gvk.Kind == "ValidatingAdmissionPolicy" || gvk.Kind == "ValidatingAdmissionPolicyBinding" {
if gvk.Kind == "ValidatingWebhookConfiguration" || gvk.Kind == "MutatingWebhookConfiguration" || gvk.Kind == "ValidatingAdmissionPolicy" || gvk.Kind == "ValidatingAdmissionPolicyBinding" || gvk.Kind == "MutatingAdmissionPolicy" || gvk.Kind == "MutatingAdmissionPolicyBinding" {
return true
}
}

View File

@ -148,6 +148,8 @@ var (
gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies"): true,
gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies/status"): true,
gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicybindings"): true,
gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicies"): true,
gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicybindings"): true,
}
parentResources = map[schema.GroupVersionResource]schema.GroupVersionResource{

View File

@ -146,6 +146,8 @@ var (
gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies"): true,
gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicies/status"): true,
gvr("admissionregistration.k8s.io", "v1", "validatingadmissionpolicybindings"): true,
gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicies"): true,
gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicybindings"): true,
// transient resource exemption
gvr("authentication.k8s.io", "v1", "selfsubjectreviews"): true,
gvr("authentication.k8s.io", "v1beta1", "selfsubjectreviews"): true,

View File

@ -103,6 +103,8 @@ func TestGenericControlplaneStartUp(t *testing.T) {
"validatingadmissionpolicies.admissionregistration.k8s.io",
"validatingadmissionpolicybindings.admissionregistration.k8s.io",
"validatingwebhookconfigurations.admissionregistration.k8s.io",
"mutatingadmissionpolicies.admissionregistration.k8s.io",
"mutatingadmissionpolicybindings.admissionregistration.k8s.io",
)
if diff := cmp.Diff(sets.List(expected), sets.List(grs)); diff != "" {
t.Fatalf("unexpected API groups: +want, -got\n%s", diff)

View File

@ -362,6 +362,14 @@ func GetEtcdStorageDataForNamespace(namespace string) map[schema.GroupVersionRes
ExpectedEtcdPath: "/registry/validatingadmissionpolicybindings/pb1a1",
ExpectedGVK: gvkP("admissionregistration.k8s.io", "v1", "ValidatingAdmissionPolicyBinding"),
},
gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicies"): {
Stub: `{"metadata":{"name":"map1","creationTimestamp":null},"spec":{"paramKind":{"apiVersion":"test.example.com/v1","kind":"Example"},"matchConstraints":{"resourceRules": [{"resourceNames": ["fakeName"], "apiGroups":["apps"],"apiVersions":["v1"],"operations":["CREATE", "UPDATE"], "resources":["deployments"]}]},"reinvocationPolicy": "IfNeeded","mutations":[{"applyConfiguration": {"expression":"Object{metadata: Object.metadata{labels: {'example':'true'}}}"}, "patchType":"ApplyConfiguration"}]}}`,
ExpectedEtcdPath: "/registry/mutatingadmissionpolicies/map1",
},
gvr("admissionregistration.k8s.io", "v1alpha1", "mutatingadmissionpolicybindings"): {
Stub: `{"metadata":{"name":"mpb1","creationTimestamp":null},"spec":{"policyName":"replicalimit-policy.example.com","paramRef":{"name":"replica-limit-test.example.com"}}}`,
ExpectedEtcdPath: "/registry/mutatingadmissionpolicybindings/mpb1",
},
// --
// k8s.io/kubernetes/pkg/apis/scheduling/v1