ValidatingAdmissionPolicy: support namespace access (#118267)

* Support namespace access from cel expression in validatingadmissionpolicy.

* Whitelist the exposed fields in namespace object and add test

* better handling of cluster-scoped resources.

* [API REVIEW] namespaceObject in Expression doc.

* compatibility with composition.

* generated: ./hack/update-codegen.sh && ./hack/update-openapi-spec.sh

* workaround namespace of namespace is unexpectedly set.

* basic test coverage for namespaceObject.

---------

Co-authored-by: Jiahui Feng <jhf@google.com>

Kubernetes-commit: 13172cba5c0e1c6a076dbda4aeebbccaf658c7f1
This commit is contained in:
Cici Huang 2023-07-14 17:53:08 -07:00 committed by Kubernetes Publisher
parent 2af49f82c0
commit 04b26c4697
20 changed files with 309 additions and 42 deletions

4
go.mod
View File

@ -41,7 +41,7 @@ require (
google.golang.org/protobuf v1.30.0
gopkg.in/natefinch/lumberjack.v2 v2.2.1
gopkg.in/square/go-jose.v2 v2.6.0
k8s.io/api v0.0.0-20230714211713-ac1defa44e72
k8s.io/api v0.0.0-20230715005308-48d7a4d6b0f6
k8s.io/apimachinery v0.0.0-20230714211010-7924d2c22746
k8s.io/client-go v0.0.0-20230714212436-f19b40cda940
k8s.io/component-base v0.0.0-20230714213649-faf645bcb8bf
@ -125,7 +125,7 @@ require (
)
replace (
k8s.io/api => k8s.io/api v0.0.0-20230714211713-ac1defa44e72
k8s.io/api => k8s.io/api v0.0.0-20230715005308-48d7a4d6b0f6
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20230714211010-7924d2c22746
k8s.io/client-go => k8s.io/client-go v0.0.0-20230714212436-f19b40cda940
k8s.io/component-base => k8s.io/component-base v0.0.0-20230714213649-faf645bcb8bf

4
go.sum
View File

@ -668,8 +668,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
k8s.io/api v0.0.0-20230714211713-ac1defa44e72 h1:VuW3Hv7ZC0+Mf7MwYisaDUAczwTtd0Q+puDnQMCIXYo=
k8s.io/api v0.0.0-20230714211713-ac1defa44e72/go.mod h1:nHelHGU5nRmkT223Jbg1sZZjj9DvhsKLsEekrET50A4=
k8s.io/api v0.0.0-20230715005308-48d7a4d6b0f6 h1:Vyx1tzAt+Z9+Lr1vyjxRu3Oqw2E5UFqQSOWo7ObFvsY=
k8s.io/api v0.0.0-20230715005308-48d7a4d6b0f6/go.mod h1:nHelHGU5nRmkT223Jbg1sZZjj9DvhsKLsEekrET50A4=
k8s.io/apimachinery v0.0.0-20230714211010-7924d2c22746 h1:ZRR1OH9l7t2q4sW0wB74g7ZDDSAWvAuO/jAcSHt7FVk=
k8s.io/apimachinery v0.0.0-20230714211010-7924d2c22746/go.mod h1:82hjKsW08vcDFh8wS9BtEee9UTcm0IlvyBnJ/zawF6E=
k8s.io/client-go v0.0.0-20230714212436-f19b40cda940 h1:EUg/9w1/iqWVzXNXjS2UvB02elWNuW4gTPG3cFlhIEo=

View File

@ -33,6 +33,7 @@ const (
OldObjectVarName = "oldObject"
ParamsVarName = "params"
RequestVarName = "request"
NamespaceVarName = "namespaceObject"
AuthorizerVarName = "authorizer"
RequestResourceAuthorizerVarName = "authorizer.requestResource"
VariableVarName = "variables"
@ -85,6 +86,56 @@ func BuildRequestType() *apiservercel.DeclType {
))
}
// BuildNamespaceType generates a DeclType for Namespace.
// Certain nested fields in Namespace (e.g. managedFields, ownerReferences etc.) are omitted in the generated DeclType
// by design.
func BuildNamespaceType() *apiservercel.DeclType {
field := func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField {
return apiservercel.NewDeclField(name, declType, required, nil, nil)
}
fields := func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField {
result := make(map[string]*apiservercel.DeclField, len(fields))
for _, f := range fields {
result[f.Name] = f
}
return result
}
specType := apiservercel.NewObjectType("kubernetes.NamespaceSpec", fields(
field("finalizers", apiservercel.NewListType(apiservercel.StringType, -1), true),
))
conditionType := apiservercel.NewObjectType("kubernetes.NamespaceCondition", fields(
field("status", apiservercel.StringType, true),
field("type", apiservercel.StringType, true),
field("lastTransitionTime", apiservercel.TimestampType, true),
field("message", apiservercel.StringType, true),
field("reason", apiservercel.StringType, true),
))
statusType := apiservercel.NewObjectType("kubernetes.NamespaceStatus", fields(
field("conditions", apiservercel.NewListType(conditionType, -1), true),
field("phase", apiservercel.StringType, true),
))
metadataType := apiservercel.NewObjectType("kubernetes.NamespaceMetadata", fields(
field("name", apiservercel.StringType, true),
field("generateName", apiservercel.StringType, true),
field("namespace", apiservercel.StringType, true),
field("labels", apiservercel.NewMapType(apiservercel.StringType, apiservercel.StringType, -1), true),
field("annotations", apiservercel.NewMapType(apiservercel.StringType, apiservercel.StringType, -1), true),
field("UID", apiservercel.StringType, true),
field("creationTimestamp", apiservercel.TimestampType, true),
field("deletionGracePeriodSeconds", apiservercel.IntType, true),
field("deletionTimestamp", apiservercel.TimestampType, true),
field("generation", apiservercel.IntType, true),
field("resourceVersion", apiservercel.StringType, true),
field("finalizers", apiservercel.NewListType(apiservercel.StringType, -1), true),
))
return apiservercel.NewObjectType("kubernetes.Namespace", fields(
field("metadata", metadataType, true),
field("spec", specType, true),
field("status", statusType, true),
))
}
// CompilationResult represents a compiled validations expression.
type CompilationResult struct {
Program cel.Program
@ -168,6 +219,7 @@ func (c compiler) CompileCELExpression(expressionAccessor ExpressionAccessor, op
func mustBuildEnvs(baseEnv *environment.EnvSet) variableDeclEnvs {
requestType := BuildRequestType()
namespaceType := BuildNamespaceType()
envs := make(variableDeclEnvs, 4) // since the number of variable combinations is small, pre-build a environment for each
for _, hasParams := range []bool{false, true} {
for _, hasAuthorizer := range []bool{false, true} {
@ -183,6 +235,7 @@ func mustBuildEnvs(baseEnv *environment.EnvSet) variableDeclEnvs {
envOpts = append(envOpts,
cel.Variable(ObjectVarName, cel.DynType),
cel.Variable(OldObjectVarName, cel.DynType),
cel.Variable(NamespaceVarName, namespaceType.CelType()),
cel.Variable(RequestVarName, requestType.CelType()))
extended, err := baseEnv.Extend(
@ -192,6 +245,7 @@ func mustBuildEnvs(baseEnv *environment.EnvSet) variableDeclEnvs {
IntroducedVersion: version.MajorMinor(1, 0),
EnvOptions: envOpts,
DeclTypes: []*apiservercel.DeclType{
namespaceType,
requestType,
},
},

View File

@ -49,6 +49,11 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
expressions: []string{"object.foo < params.x"},
hasParams: true,
},
{
name: "namespaceObject",
expressions: []string{"namespaceObject.metadata.name.startsWith('test')"},
hasParams: true,
},
{
name: "without params",
errorExpressions: map[string]string{"object.foo < params.x": "undeclared reference to 'params'"},
@ -135,6 +140,41 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
},
envType: environment.NewExpressions,
},
{
name: "valid namespaceObject",
expressions: []string{
"namespaceObject.metadata != null",
"namespaceObject.metadata.name == 'test'",
"namespaceObject.metadata.generateName == 'test'",
"namespaceObject.metadata.namespace == 'testns'",
"'test' in namespaceObject.metadata.labels",
"'test' in namespaceObject.metadata.annotations",
"namespaceObject.metadata.UID == '12345'",
"type(namespaceObject.metadata.creationTimestamp) == google.protobuf.Timestamp",
"type(namespaceObject.metadata.deletionTimestamp) == google.protobuf.Timestamp",
"namespaceObject.metadata.deletionGracePeriodSeconds == 5",
"namespaceObject.metadata.generation == 2",
"namespaceObject.metadata.resourceVersion == 'v1'",
"namespaceObject.metadata.finalizers[0] == 'testEnv'",
"namespaceObject.spec.finalizers[0] == 'testEnv'",
"namespaceObject.status.phase == 'Active'",
"namespaceObject.status.conditions[0].status == 'True'",
"namespaceObject.status.conditions[0].type == 'NamespaceDeletionDiscoveryFailure'",
"type(namespaceObject.status.conditions[0].lastTransitionTime) == google.protobuf.Timestamp",
"namespaceObject.status.conditions[0].message == 'Unknow'",
"namespaceObject.status.conditions[0].reason == 'Invalid'",
},
},
{
name: "invalid namespaceObject",
errorExpressions: map[string]string{
"namespaceObject.foo1 == 'nope'": "undefined field 'foo1'",
"namespaceObject.metadata.foo2 == 'nope'": "undefined field 'foo2'",
"namespaceObject.spec.foo3 == 'nope'": "undefined field 'foo3'",
"namespaceObject.status.foo4 == 'nope'": "undefined field 'foo4'",
"namespaceObject.status.conditions[0].foo5 == 'nope'": "undefined field 'foo5'",
},
},
}
// Include the test library, which includes the test() function in the storage environment during test

View File

@ -25,6 +25,7 @@ import (
"github.com/google/cel-go/common/types/ref"
v1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/util/version"
"k8s.io/apiserver/pkg/admission"
apiservercel "k8s.io/apiserver/pkg/cel"
@ -149,9 +150,9 @@ func (c *compositionContext) Variables(activation any) ref.Val {
return lazyMap
}
func (f *CompositedFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
func (f *CompositedFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
ctx = f.compositionEnv.CreateContext(ctx)
return f.Filter.ForInput(ctx, versionedAttr, request, optionalVars, runtimeCELCostBudget)
return f.Filter.ForInput(ctx, versionedAttr, request, optionalVars, namespace, runtimeCELCostBudget)
}
func (c *compositionContext) reportCost(cost int64) {

View File

@ -141,7 +141,7 @@ func TestCompositedPolicies(t *testing.T) {
if costBudget == 0 {
costBudget = celconfig.RuntimeCELCostBudget
}
result, _, err := f.ForInput(context.Background(), versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, costBudget)
result, _, err := f.ForInput(context.Background(), versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, nil, costBudget)
if !tc.expectErr && err != nil {
t.Fatalf("failed evaluation: %v", err)
}

View File

@ -27,6 +27,7 @@ import (
admissionv1 "k8s.io/api/admission/v1"
authenticationv1 "k8s.io/api/authentication/v1"
v1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
@ -46,7 +47,7 @@ func NewFilterCompiler(env *environment.EnvSet) FilterCompiler {
}
type evaluationActivation struct {
object, oldObject, params, request, authorizer, requestResourceAuthorizer, variables interface{}
object, oldObject, params, request, namespace, authorizer, requestResourceAuthorizer, variables interface{}
}
// ResolveName returns a value from the activation by qualified name, or false if the name
@ -61,6 +62,8 @@ func (a *evaluationActivation) ResolveName(name string) (interface{}, bool) {
return a.params, true // params may be null
case RequestVarName:
return a.request, true
case NamespaceVarName:
return a.namespace, true
case AuthorizerVarName:
return a.authorizer, a.authorizer != nil
case RequestResourceAuthorizerVarName:
@ -126,7 +129,7 @@ func objectToResolveVal(r runtime.Object) (interface{}, error) {
// ForInput evaluates the compiled CEL expressions converting them into CELEvaluations
// errors per evaluation are returned on the Evaluation object
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func (f *filter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
func (f *filter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error) {
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
evaluations := make([]EvaluationResult, len(f.compilationResults))
var err error
@ -156,11 +159,16 @@ func (f *filter) ForInput(ctx context.Context, versionedAttr *admission.Versione
if err != nil {
return nil, -1, err
}
namespaceVal, err := objectToResolveVal(namespace)
if err != nil {
return nil, -1, err
}
va := &evaluationActivation{
object: objectVal,
oldObject: oldObjectVal,
params: paramsVal,
request: requestVal.Object,
namespace: namespaceVal,
authorizer: authorizerVal,
requestResourceAuthorizer: requestResourceAuthorizerVal,
}
@ -307,6 +315,33 @@ func CreateAdmissionRequest(attr admission.Attributes) *admissionv1.AdmissionReq
}
}
// CreateNamespaceObject creates a Namespace object that is suitable for the CEL evaluation.
// If the namespace is nil, CreateNamespaceObject returns nil
func CreateNamespaceObject(namespace *v1.Namespace) *v1.Namespace {
if namespace == nil {
return nil
}
return &v1.Namespace{
Status: namespace.Status,
Spec: namespace.Spec,
ObjectMeta: metav1.ObjectMeta{
Name: namespace.Name,
GenerateName: namespace.GenerateName,
Namespace: namespace.Namespace,
UID: namespace.UID,
ResourceVersion: namespace.ResourceVersion,
Generation: namespace.Generation,
CreationTimestamp: namespace.CreationTimestamp,
DeletionTimestamp: namespace.DeletionTimestamp,
DeletionGracePeriodSeconds: namespace.DeletionGracePeriodSeconds,
Labels: namespace.Labels,
Annotations: namespace.Annotations,
Finalizers: namespace.Finalizers,
},
}
}
// CompilationErrors returns a list of all the errors from the compilation of the evaluator
func (e *filter) CompilationErrors() []error {
compilationErrors := []error{}

View File

@ -149,6 +149,28 @@ func TestFilter(t *testing.T) {
},
}
nsObject := &corev1.Namespace{
ObjectMeta: metav1.ObjectMeta{
Name: "test",
Labels: map[string]string{
"env": "test",
"foo": "demo",
},
Annotations: map[string]string{
"annotation1": "testAnnotation1",
},
Finalizers: []string{"f1"},
},
Spec: corev1.NamespaceSpec{
Finalizers: []corev1.FinalizerName{
corev1.FinalizerKubernetes,
},
},
Status: corev1.NamespaceStatus{
Phase: corev1.NamespaceActive,
},
}
var nilUnstructured *unstructured.Unstructured
cases := []struct {
name string
@ -159,6 +181,7 @@ func TestFilter(t *testing.T) {
hasParamKind bool
authorizer authorizer.Authorizer
testPerCallLimit uint64
namespaceObject *corev1.Namespace
}{
{
name: "valid syntax for object",
@ -674,6 +697,64 @@ func TestFilter(t *testing.T) {
params: crdParams,
testPerCallLimit: 1,
},
{
name: "test namespaceObject",
validations: []ExpressionAccessor{
&condition{
Expression: "namespaceObject.metadata.name == 'test'",
},
&condition{
Expression: "'env' in namespaceObject.metadata.labels && namespaceObject.metadata.labels.env == 'test'",
},
&condition{
Expression: "('fake' in namespaceObject.metadata.labels) && namespaceObject.metadata.labels.fake == 'test'",
},
&condition{
Expression: "namespaceObject.spec.finalizers[0] == 'kubernetes'",
},
&condition{
Expression: "namespaceObject.status.phase == 'Active'",
},
&condition{
Expression: "size(namespaceObject.metadata.managedFields) == 1",
},
&condition{
Expression: "size(namespaceObject.metadata.ownerReferences) == 1",
},
&condition{
Expression: "'env' in namespaceObject.metadata.annotations",
},
},
attributes: newValidAttribute(&podObject, false),
results: []EvaluationResult{
{
EvalResult: celtypes.True,
},
{
EvalResult: celtypes.True,
},
{
EvalResult: celtypes.False,
},
{
EvalResult: celtypes.True,
},
{
EvalResult: celtypes.True,
},
{
Error: errors.New("undefined field 'managedFields'"),
},
{
Error: errors.New("undefined field 'ownerReferences'"),
},
{
EvalResult: celtypes.False,
},
},
hasParamKind: false,
namespaceObject: nsObject,
},
}
for _, tc := range cases {
@ -706,7 +787,7 @@ func TestFilter(t *testing.T) {
optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer}
ctx := context.TODO()
evalResults, _, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, celconfig.RuntimeCELCostBudget)
evalResults, _, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, CreateNamespaceObject(tc.namespaceObject), celconfig.RuntimeCELCostBudget)
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
@ -852,7 +933,7 @@ func TestRuntimeCELCostBudget(t *testing.T) {
}
optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer}
ctx := context.TODO()
evalResults, remaining, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, tc.testRuntimeCELCostBudget)
evalResults, remaining, err := f.ForInput(ctx, versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars, nil, tc.testRuntimeCELCostBudget)
if tc.exceedBudget && err == nil {
t.Errorf("Expected RuntimeCELCostBudge to be exceeded but got nil")
}

View File

@ -24,6 +24,7 @@ import (
"github.com/google/cel-go/common/types/ref"
v1 "k8s.io/api/admission/v1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
"k8s.io/apiserver/pkg/authorization/authorizer"
@ -87,7 +88,7 @@ type Filter interface {
// ForInput converts compiled CEL-typed values into evaluated CEL-typed value.
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
// If cost budget is calculated, the filter should return the remaining budget.
ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error)
ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings, namespace *corev1.Namespace, runtimeCELCostBudget int64) ([]EvaluationResult, int64, error)
// CompilationErrors returns a list of errors from the compilation of the evaluator
CompilationErrors() []error

View File

@ -253,7 +253,7 @@ type fakeFilter struct {
keyId string
}
func (f *fakeFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs cel.OptionalVariableBindings, runtimeCELCostBudget int64) ([]cel.EvaluationResult, int64, error) {
func (f *fakeFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs cel.OptionalVariableBindings, namespace *v1.Namespace, runtimeCELCostBudget int64) ([]cel.EvaluationResult, int64, error) {
return []cel.EvaluationResult{}, 0, nil
}
@ -265,10 +265,10 @@ var _ Validator = &fakeValidator{}
type fakeValidator struct {
validationFilter, auditAnnotationFilter, messageFilter *fakeFilter
ValidateFunc func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult
ValidateFunc func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult
}
func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, validateFunc func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult) {
func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, validateFunc func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult) {
//Key must be something that we can decipher from the inputs to Validate so using message which will be on the validationCondition object of evalResult
var key string
if len(definition.Spec.Validations) > 0 {
@ -285,8 +285,8 @@ func (f *fakeValidator) RegisterDefinition(definition *v1alpha1.ValidatingAdmiss
validatorMap[key] = f
}
func (f *fakeValidator) Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
return f.ValidateFunc(ctx, versionedAttr, versionedParams, runtimeCELCostBudget, authz)
func (f *fakeValidator) Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
return f.ValidateFunc(ctx, versionedAttr, versionedParams, namespace, runtimeCELCostBudget, authz)
}
var _ Matcher = &fakeMatcher{}
@ -295,6 +295,10 @@ func (f *fakeMatcher) ValidateInitialization() error {
return nil
}
func (f *fakeMatcher) GetNamespace(name string) (*v1.Namespace, error) {
return nil, nil
}
type fakeMatcher struct {
DefaultMatch bool
DefinitionMatchFuncs map[namespacedName]func(*v1alpha1.ValidatingAdmissionPolicy, admission.Attributes) bool
@ -770,7 +774,7 @@ func TestBasicPolicyDefinitionFailure(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
@ -840,7 +844,7 @@ func TestDefinitionDoesntMatch(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
@ -953,7 +957,7 @@ func TestReconfigureBinding(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
@ -1063,7 +1067,7 @@ func TestRemoveDefinition(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
@ -1132,7 +1136,7 @@ func TestRemoveBinding(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
@ -1242,7 +1246,7 @@ func TestInvalidParamSourceInstanceName(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
@ -1310,7 +1314,7 @@ func TestEmptyParamSource(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
@ -1412,7 +1416,7 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) {
}
})
validator1.RegisterDefinition(&policy1, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
validator1.RegisterDefinition(&policy1, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
evaluations1.Add(1)
return ValidateResult{
Decisions: []PolicyDecision{
@ -1431,7 +1435,7 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) {
}
})
validator2.RegisterDefinition(&policy2, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
validator2.RegisterDefinition(&policy2, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
evaluations2.Add(1)
return ValidateResult{
Decisions: []PolicyDecision{
@ -1541,7 +1545,7 @@ func TestNativeTypeParam(t *testing.T) {
}
})
validator.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
validator.RegisterDefinition(&nativeTypeParamPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
evaluations.Add(1)
if _, ok := versionedParams.(*v1.ConfigMap); ok {
return ValidateResult{
@ -1623,7 +1627,7 @@ func TestAuditValidationAction(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
@ -1694,7 +1698,7 @@ func TestWarnValidationAction(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
@ -1753,7 +1757,7 @@ func TestAllValidationActions(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
return ValidateResult{
Decisions: []PolicyDecision{
{
@ -1824,7 +1828,7 @@ func TestAuditAnnotations(t *testing.T) {
}
})
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
validator.RegisterDefinition(denyPolicy, func(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *v1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
o, err := meta.Accessor(versionedParams)
if err != nil {
t.Fatal(err)

View File

@ -26,6 +26,7 @@ import (
"time"
"k8s.io/api/admissionregistration/v1alpha1"
v1 "k8s.io/api/core/v1"
k8serrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -319,6 +320,23 @@ func (c *celAdmissionController) Validate(
continue
}
}
var namespace *v1.Namespace
namespaceName := a.GetNamespace()
// Special case, the namespace object has the namespace of itself (maybe a bug).
// unset it if the incoming object is a namespace
if gvk := a.GetKind(); gvk.Kind == "Namespace" && gvk.Version == "v1" && gvk.Group == "" {
namespaceName = ""
}
// if it is cluster scoped, namespaceName will be empty
// Otherwise, get the Namespace resource.
if namespaceName != "" {
namespace, err = c.policyController.matcher.GetNamespace(namespaceName)
if err != nil {
return err
}
}
if versionedAttr == nil {
va, err := admission.NewVersionedAttributes(a, matchKind, o)
@ -330,7 +348,7 @@ func (c *celAdmissionController) Validate(
versionedAttr = va
}
validationResult := bindingInfo.validator.Validate(ctx, versionedAttr, param, celconfig.RuntimeCELCostBudget, authz)
validationResult := bindingInfo.validator.Validate(ctx, versionedAttr, param, namespace, celconfig.RuntimeCELCostBudget, authz)
for i, decision := range validationResult.Decisions {
switch decision.Action {

View File

@ -22,6 +22,7 @@ import (
celgo "github.com/google/cel-go/cel"
"k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -90,6 +91,10 @@ type Matcher interface {
// BindingMatches says whether this policy definition matches the provided admission
// resource request
BindingMatches(a admission.Attributes, o admission.ObjectInterfaces, definition *v1alpha1.ValidatingAdmissionPolicyBinding) (bool, error)
// GetNamespace retrieves the Namespace resource by the given name. The name may be empty, in which case
// GetNamespace must return nil, nil
GetNamespace(name string) (*corev1.Namespace, error)
}
// ValidateResult defines the result of a Validator.Validate operation.
@ -104,5 +109,5 @@ type ValidateResult struct {
type Validator interface {
// Validate is used to take cel evaluations and convert into decisions
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult
Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *corev1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult
}

View File

@ -18,6 +18,7 @@ package validatingadmissionpolicy
import (
"k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/labels"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -76,3 +77,7 @@ func (c *matcher) BindingMatches(a admission.Attributes, o admission.ObjectInter
isMatch, _, err := c.Matcher.Matches(a, o, &criteria)
return isMatch, err
}
func (c *matcher) GetNamespace(name string) (*corev1.Namespace, error) {
return c.Matcher.GetNamespace(name)
}

View File

@ -21,6 +21,7 @@ import (
v1 "k8s.io/api/admissionregistration/v1"
"k8s.io/api/admissionregistration/v1alpha1"
corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apiserver/pkg/admission"
"k8s.io/client-go/kubernetes"
@ -44,6 +45,10 @@ type Matcher struct {
objectMatcher *object.Matcher
}
func (m *Matcher) GetNamespace(name string) (*corev1.Namespace, error) {
return m.namespaceMatcher.GetNamespace(name)
}
// NewMatcher initialize the matcher with dependencies requires
func NewMatcher(
namespaceLister listersv1.NamespaceLister,

View File

@ -347,10 +347,15 @@ func sortGVKList(list []schema.GroupVersionKind) []schema.GroupVersionKind {
func buildEnv(hasParams bool, hasAuthorizer bool, types typeOverwrite) (*cel.Env, error) {
baseEnv := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion())
requestType := plugincel.BuildRequestType()
namespaceType := plugincel.BuildNamespaceType()
var varOpts []cel.EnvOption
var declTypes []*apiservercel.DeclType
// namespace, hand-crafted type
declTypes = append(declTypes, namespaceType)
varOpts = append(varOpts, createVariableOpts(namespaceType, plugincel.NamespaceVarName)...)
// request, hand-crafted type
declTypes = append(declTypes, requestType)
varOpts = append(varOpts, createVariableOpts(requestType, plugincel.RequestVarName)...)

View File

@ -24,6 +24,7 @@ import (
celtypes "github.com/google/cel-go/common/types"
v1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apiserver/pkg/admission"
@ -70,7 +71,8 @@ func auditAnnotationEvaluationForError(f v1.FailurePolicyType) PolicyAuditAnnota
// Validate takes a list of Evaluation and a failure policy and converts them into actionable PolicyDecisions
// runtimeCELCostBudget was added for testing purpose only. Callers should always use const RuntimeCELCostBudget from k8s.io/apiserver/pkg/apis/cel/config.go as input.
func (v *validator) Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
func (v *validator) Validate(ctx context.Context, versionedAttr *admission.VersionedAttributes, versionedParams runtime.Object, namespace *corev1.Namespace, runtimeCELCostBudget int64, authz authorizer.Authorizer) ValidateResult {
var f v1.FailurePolicyType
if v.failPolicy == nil {
f = v1.Fail
@ -101,7 +103,9 @@ func (v *validator) Validate(ctx context.Context, versionedAttr *admission.Versi
optionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams, Authorizer: authz}
expressionOptionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams}
admissionRequest := cel.CreateAdmissionRequest(versionedAttr.Attributes)
evalResults, remainingBudget, err := v.validationFilter.ForInput(ctx, versionedAttr, admissionRequest, optionalVars, runtimeCELCostBudget)
// Decide which fields are exposed
ns := cel.CreateNamespaceObject(namespace)
evalResults, remainingBudget, err := v.validationFilter.ForInput(ctx, versionedAttr, admissionRequest, optionalVars, ns, runtimeCELCostBudget)
if err != nil {
return ValidateResult{
Decisions: []PolicyDecision{
@ -114,7 +118,7 @@ func (v *validator) Validate(ctx context.Context, versionedAttr *admission.Versi
}
}
decisions := make([]PolicyDecision, len(evalResults))
messageResults, _, err := v.messageFilter.ForInput(ctx, versionedAttr, admissionRequest, expressionOptionalVars, remainingBudget)
messageResults, _, err := v.messageFilter.ForInput(ctx, versionedAttr, admissionRequest, expressionOptionalVars, ns, remainingBudget)
for i, evalResult := range evalResults {
var decision = &decisions[i]
// TODO: move this to generics
@ -191,7 +195,7 @@ func (v *validator) Validate(ctx context.Context, versionedAttr *admission.Versi
}
options := cel.OptionalVariableBindings{VersionedParams: versionedParams}
auditAnnotationEvalResults, _, err := v.auditAnnotationFilter.ForInput(ctx, versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), options, runtimeCELCostBudget)
auditAnnotationEvalResults, _, err := v.auditAnnotationFilter.ForInput(ctx, versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), options, namespace, runtimeCELCostBudget)
if err != nil {
return ValidateResult{
Decisions: []PolicyDecision{

View File

@ -28,6 +28,7 @@ import (
admissionv1 "k8s.io/api/admission/v1"
v1 "k8s.io/api/admissionregistration/v1"
corev1 "k8s.io/api/core/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
@ -47,7 +48,7 @@ type fakeCelFilter struct {
throwError bool
}
func (f *fakeCelFilter) ForInput(_ context.Context, _ *admission.VersionedAttributes, _ *admissionv1.AdmissionRequest, _ cel.OptionalVariableBindings, costBudget int64) ([]cel.EvaluationResult, int64, error) {
func (f *fakeCelFilter) ForInput(ctx context.Context, versionedAttr *admission.VersionedAttributes, request *admissionv1.AdmissionRequest, optionalVars cel.OptionalVariableBindings, namespace *corev1.Namespace, costBudget int64) ([]cel.EvaluationResult, int64, error) {
if costBudget <= 0 { // this filter will cost 1, so cost = 0 means fail.
return nil, -1, &apiservercel.Error{
Type: apiservercel.ErrorTypeInvalid,
@ -892,7 +893,7 @@ func TestValidate(t *testing.T) {
if tc.costBudget != 0 {
budget = tc.costBudget
}
validateResult := v.Validate(ctx, fakeVersionedAttr, nil, budget, nil)
validateResult := v.Validate(ctx, fakeVersionedAttr, nil, nil, budget, nil)
require.Equal(t, len(validateResult.Decisions), len(tc.policyDecision))
@ -944,7 +945,7 @@ func TestContextCanceled(t *testing.T) {
}
ctx, cancel := context.WithCancel(context.TODO())
cancel()
validationResult := v.Validate(ctx, fakeVersionedAttr, nil, celconfig.RuntimeCELCostBudget, nil)
validationResult := v.Validate(ctx, fakeVersionedAttr, nil, nil, celconfig.RuntimeCELCostBudget, nil)
if len(validationResult.Decisions) != 1 || !strings.Contains(validationResult.Decisions[0].Message, "operation interrupted") {
t.Errorf("Expected 'operation interrupted' but got %v", validationResult.Decisions)
}

View File

@ -81,7 +81,7 @@ func (m *matcher) Match(ctx context.Context, versionedAttr *admission.VersionedA
evalResults, _, err := m.filter.ForInput(ctx, versionedAttr, celplugin.CreateAdmissionRequest(versionedAttr.Attributes), celplugin.OptionalVariableBindings{
VersionedParams: versionedParams,
Authorizer: authz,
}, celconfig.RuntimeCELCostBudgetMatchConditions)
}, nil, celconfig.RuntimeCELCostBudgetMatchConditions)
if err != nil {
admissionmetrics.Metrics.ObserveMatchConditionEvaluationTime(ctx, time.Since(t), m.objectName, m.matcherKind, m.matcherType, string(versionedAttr.GetOperation()))

View File

@ -22,6 +22,8 @@ import (
"strings"
"testing"
api "k8s.io/api/core/v1"
v1 "k8s.io/api/admissionregistration/v1"
celtypes "github.com/google/cel-go/common/types"
@ -40,7 +42,7 @@ type fakeCelFilter struct {
throwError bool
}
func (f *fakeCelFilter) ForInput(context.Context, *admission.VersionedAttributes, *admissionv1.AdmissionRequest, cel.OptionalVariableBindings, int64) ([]cel.EvaluationResult, int64, error) {
func (f *fakeCelFilter) ForInput(context.Context, *admission.VersionedAttributes, *admissionv1.AdmissionRequest, cel.OptionalVariableBindings, *api.Namespace, int64) ([]cel.EvaluationResult, int64, error) {
if f.throwError {
return nil, 0, errors.New("test error")
}

View File

@ -20,6 +20,8 @@ import (
"context"
"fmt"
v1 "k8s.io/api/core/v1"
apierrors "k8s.io/apimachinery/pkg/api/errors"
"k8s.io/apimachinery/pkg/api/meta"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@ -42,6 +44,10 @@ type Matcher struct {
Client clientset.Interface
}
func (m *Matcher) GetNamespace(name string) (*v1.Namespace, error) {
return m.NamespaceLister.Get(name)
}
// Validate checks if the Matcher has a NamespaceLister and Client.
func (m *Matcher) Validate() error {
var errs []error