mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-08-12 13:31:52 +00:00
Adjust CEL cost calculation and versioning for authorization library
This commit is contained in:
parent
be2e32fa3e
commit
83bd512861
@ -20,12 +20,6 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"k8s.io/apimachinery/pkg/fields"
|
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
|
||||||
"k8s.io/apimachinery/pkg/selection"
|
|
||||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
|
||||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
|
||||||
"reflect"
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@ -34,19 +28,25 @@ import (
|
|||||||
celtypes "github.com/google/cel-go/common/types"
|
celtypes "github.com/google/cel-go/common/types"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"k8s.io/utils/pointer"
|
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
|
||||||
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apimachinery/pkg/selection"
|
||||||
|
"k8s.io/apimachinery/pkg/util/version"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||||
"k8s.io/apiserver/pkg/authentication/user"
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||||
"k8s.io/apiserver/pkg/cel/environment"
|
"k8s.io/apiserver/pkg/cel/environment"
|
||||||
|
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
|
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||||
|
"k8s.io/utils/pointer"
|
||||||
)
|
)
|
||||||
|
|
||||||
type condition struct {
|
type condition struct {
|
||||||
@ -182,6 +182,9 @@ func TestFilter(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
v130 := version.MajorMinor(1, 30)
|
||||||
|
v131 := version.MajorMinor(1, 31)
|
||||||
|
|
||||||
var nilUnstructured *unstructured.Unstructured
|
var nilUnstructured *unstructured.Unstructured
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
name string
|
name string
|
||||||
@ -195,6 +198,8 @@ func TestFilter(t *testing.T) {
|
|||||||
namespaceObject *corev1.Namespace
|
namespaceObject *corev1.Namespace
|
||||||
strictCost bool
|
strictCost bool
|
||||||
enableSelectors bool
|
enableSelectors bool
|
||||||
|
|
||||||
|
compatibilityVersion *version.Version
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid syntax for object",
|
name: "valid syntax for object",
|
||||||
@ -494,6 +499,38 @@ func TestFilter(t *testing.T) {
|
|||||||
APIVersion: "*",
|
APIVersion: "*",
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "test authorizer error using fieldSelector with 1.30 compatibility",
|
||||||
|
validations: []ExpressionAccessor{
|
||||||
|
&condition{
|
||||||
|
Expression: "authorizer.group('apps').resource('deployments').fieldSelector('foo=bar').labelSelector('apple=banana').subresource('status').namespace('test').name('backend').check('create').allowed()",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attributes: newValidAttribute(&podObject, false),
|
||||||
|
results: []EvaluationResult{
|
||||||
|
{
|
||||||
|
Error: fmt.Errorf("fieldSelector"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
|
||||||
|
ResourceRequest: true,
|
||||||
|
APIGroup: "apps",
|
||||||
|
Resource: "deployments",
|
||||||
|
Subresource: "status",
|
||||||
|
Namespace: "test",
|
||||||
|
Name: "backend",
|
||||||
|
Verb: "create",
|
||||||
|
APIVersion: "*",
|
||||||
|
FieldSelectorRequirements: fields.Requirements{
|
||||||
|
{Operator: "=", Field: "foo", Value: "bar"},
|
||||||
|
},
|
||||||
|
LabelSelectorRequirements: labels.Requirements{
|
||||||
|
*simpleLabelSelector,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
enableSelectors: true,
|
||||||
|
compatibilityVersion: v130,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "test authorizer allow resource check with all fields",
|
name: "test authorizer allow resource check with all fields",
|
||||||
validations: []ExpressionAccessor{
|
validations: []ExpressionAccessor{
|
||||||
@ -524,6 +561,7 @@ func TestFilter(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
enableSelectors: true,
|
enableSelectors: true,
|
||||||
|
compatibilityVersion: v131,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "test authorizer allow resource check with parse failures",
|
name: "test authorizer allow resource check with parse failures",
|
||||||
@ -551,6 +589,7 @@ func TestFilter(t *testing.T) {
|
|||||||
LabelSelectorParsingErr: errors.New("unable to parse requirement: found 'badoperator', expected: in, notin, =, ==, !=, gt, lt"),
|
LabelSelectorParsingErr: errors.New("unable to parse requirement: found 'badoperator', expected: in, notin, =, ==, !=, gt, lt"),
|
||||||
}),
|
}),
|
||||||
enableSelectors: true,
|
enableSelectors: true,
|
||||||
|
compatibilityVersion: v131,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "test authorizer allow resource check with all fields, without gate",
|
name: "test authorizer allow resource check with all fields, without gate",
|
||||||
@ -575,6 +614,7 @@ func TestFilter(t *testing.T) {
|
|||||||
Verb: "create",
|
Verb: "create",
|
||||||
APIVersion: "*",
|
APIVersion: "*",
|
||||||
}),
|
}),
|
||||||
|
compatibilityVersion: v131,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "test authorizer not allowed resource check one incorrect field",
|
name: "test authorizer not allowed resource check one incorrect field",
|
||||||
@ -837,9 +877,13 @@ func TestFilter(t *testing.T) {
|
|||||||
if tc.testPerCallLimit == 0 {
|
if tc.testPerCallLimit == 0 {
|
||||||
tc.testPerCallLimit = celconfig.PerCallLimit
|
tc.testPerCallLimit = celconfig.PerCallLimit
|
||||||
}
|
}
|
||||||
env, err := environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion(), tc.strictCost).Extend(
|
compatibilityVersion := tc.compatibilityVersion
|
||||||
|
if compatibilityVersion == nil {
|
||||||
|
compatibilityVersion = environment.DefaultCompatibilityVersion()
|
||||||
|
}
|
||||||
|
env, err := environment.MustBaseEnvSet(compatibilityVersion, tc.strictCost).Extend(
|
||||||
environment.VersionedOptions{
|
environment.VersionedOptions{
|
||||||
IntroducedVersion: environment.DefaultCompatibilityVersion(),
|
IntroducedVersion: compatibilityVersion,
|
||||||
ProgramOptions: []celgo.ProgramOption{celgo.CostLimit(tc.testPerCallLimit)},
|
ProgramOptions: []celgo.ProgramOption{celgo.CostLimit(tc.testPerCallLimit)},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -868,12 +912,16 @@ func TestFilter(t *testing.T) {
|
|||||||
}
|
}
|
||||||
require.Equal(t, len(evalResults), len(tc.results))
|
require.Equal(t, len(evalResults), len(tc.results))
|
||||||
for i, result := range tc.results {
|
for i, result := range tc.results {
|
||||||
if result.EvalResult != evalResults[i].EvalResult {
|
|
||||||
t.Errorf("Expected result '%v' but got '%v'", result.EvalResult, evalResults[i].EvalResult)
|
|
||||||
}
|
|
||||||
if result.Error != nil && !strings.Contains(evalResults[i].Error.Error(), result.Error.Error()) {
|
if result.Error != nil && !strings.Contains(evalResults[i].Error.Error(), result.Error.Error()) {
|
||||||
t.Errorf("Expected result '%v' but got '%v'", result.Error, evalResults[i].Error)
|
t.Errorf("Expected result '%v' but got '%v'", result.Error, evalResults[i].Error)
|
||||||
}
|
}
|
||||||
|
if result.Error == nil && evalResults[i].Error != nil {
|
||||||
|
t.Errorf("Expected result '%v' but got error '%v'", result.EvalResult, evalResults[i].Error)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if result.EvalResult != evalResults[i].EvalResult {
|
||||||
|
t.Errorf("Expected result '%v' but got '%v'", result.EvalResult, evalResults[i].EvalResult)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/google/cel-go/cel"
|
"github.com/google/cel-go/cel"
|
||||||
"github.com/google/cel-go/checker"
|
"github.com/google/cel-go/checker"
|
||||||
@ -30,6 +31,8 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/util/version"
|
"k8s.io/apimachinery/pkg/util/version"
|
||||||
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
celconfig "k8s.io/apiserver/pkg/apis/cel"
|
||||||
"k8s.io/apiserver/pkg/cel/library"
|
"k8s.io/apiserver/pkg/cel/library"
|
||||||
|
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||||
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
utilversion "k8s.io/apiserver/pkg/util/version"
|
utilversion "k8s.io/apiserver/pkg/util/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -146,6 +149,38 @@ var baseOptsWithoutStrictCost = []VersionedOptions{
|
|||||||
library.Format(),
|
library.Format(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
// Authz selectors
|
||||||
|
{
|
||||||
|
IntroducedVersion: version.MajorMinor(1, 31),
|
||||||
|
FeatureEnabled: func() bool {
|
||||||
|
enabled := utilfeature.DefaultFeatureGate.Enabled(genericfeatures.AuthorizeWithSelectors)
|
||||||
|
authzSelectorsLibraryInit.Do(func() {
|
||||||
|
// Record the first time feature enablement was checked for this library.
|
||||||
|
// This is checked from integration tests to ensure no cached cel envs
|
||||||
|
// are constructed before feature enablement is effectively set.
|
||||||
|
authzSelectorsLibraryEnabled.Store(enabled)
|
||||||
|
// Uncomment to debug where the first initialization is coming from if needed.
|
||||||
|
// debug.PrintStack()
|
||||||
|
})
|
||||||
|
return enabled
|
||||||
|
},
|
||||||
|
EnvOptions: []cel.EnvOption{
|
||||||
|
library.AuthzSelectors(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
authzSelectorsLibraryInit sync.Once
|
||||||
|
authzSelectorsLibraryEnabled atomic.Value
|
||||||
|
)
|
||||||
|
|
||||||
|
// AuthzSelectorsLibraryEnabled returns whether the AuthzSelectors library was enabled when it was constructed.
|
||||||
|
// If it has not been contructed yet, this returns `false, false`.
|
||||||
|
// This is solely for the benefit of the integration tests making sure feature gates get correctly parsed before AuthzSelector ever has to check for enablement.
|
||||||
|
func AuthzSelectorsLibraryEnabled() (enabled, constructed bool) {
|
||||||
|
enabled, constructed = authzSelectorsLibraryEnabled.Load().(bool)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var StrictCostOpt = VersionedOptions{
|
var StrictCostOpt = VersionedOptions{
|
||||||
|
@ -175,7 +175,15 @@ type VersionedOptions struct {
|
|||||||
//
|
//
|
||||||
// Optional.
|
// Optional.
|
||||||
RemovedVersion *version.Version
|
RemovedVersion *version.Version
|
||||||
|
// FeatureEnabled returns true if these options are enabled by feature gates,
|
||||||
|
// and returns false if these options are not enabled due to feature gates.
|
||||||
|
//
|
||||||
|
// This takes priority over IntroducedVersion / RemovedVersion for the NewExpressions environment.
|
||||||
|
//
|
||||||
|
// The StoredExpressions environment ignores this function.
|
||||||
|
//
|
||||||
|
// Optional.
|
||||||
|
FeatureEnabled func() bool
|
||||||
// EnvOptions provides CEL EnvOptions. This may be used to add a cel.Variable, a
|
// EnvOptions provides CEL EnvOptions. This may be used to add a cel.Variable, a
|
||||||
// cel.Library, or to enable other CEL EnvOptions such as language settings.
|
// cel.Library, or to enable other CEL EnvOptions such as language settings.
|
||||||
//
|
//
|
||||||
@ -210,7 +218,7 @@ type VersionedOptions struct {
|
|||||||
// making multiple calls to Extend.
|
// making multiple calls to Extend.
|
||||||
func (e *EnvSet) Extend(options ...VersionedOptions) (*EnvSet, error) {
|
func (e *EnvSet) Extend(options ...VersionedOptions) (*EnvSet, error) {
|
||||||
if len(options) > 0 {
|
if len(options) > 0 {
|
||||||
newExprOpts, err := e.filterAndBuildOpts(e.newExpressions, e.compatibilityVersion, options)
|
newExprOpts, err := e.filterAndBuildOpts(e.newExpressions, e.compatibilityVersion, true, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -218,7 +226,7 @@ func (e *EnvSet) Extend(options ...VersionedOptions) (*EnvSet, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
storedExprOpt, err := e.filterAndBuildOpts(e.storedExpressions, version.MajorMinor(math.MaxUint, math.MaxUint), options)
|
storedExprOpt, err := e.filterAndBuildOpts(e.storedExpressions, version.MajorMinor(math.MaxUint, math.MaxUint), false, options)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -231,13 +239,26 @@ func (e *EnvSet) Extend(options ...VersionedOptions) (*EnvSet, error) {
|
|||||||
return e, nil
|
return e, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *EnvSet) filterAndBuildOpts(base *cel.Env, compatVer *version.Version, opts []VersionedOptions) (cel.EnvOption, error) {
|
func (e *EnvSet) filterAndBuildOpts(base *cel.Env, compatVer *version.Version, honorFeatureGateEnablement bool, opts []VersionedOptions) (cel.EnvOption, error) {
|
||||||
var envOpts []cel.EnvOption
|
var envOpts []cel.EnvOption
|
||||||
var progOpts []cel.ProgramOption
|
var progOpts []cel.ProgramOption
|
||||||
var declTypes []*apiservercel.DeclType
|
var declTypes []*apiservercel.DeclType
|
||||||
|
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
|
var allowedByFeatureGate, allowedByVersion bool
|
||||||
|
if opt.FeatureEnabled != nil && honorFeatureGateEnablement {
|
||||||
|
// Feature-gate-enabled libraries must follow compatible default feature enablement.
|
||||||
|
// Enabling alpha features in their first release enables libraries the previous API server is unaware of.
|
||||||
|
allowedByFeatureGate = opt.FeatureEnabled()
|
||||||
|
if !allowedByFeatureGate {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
if compatVer.AtLeast(opt.IntroducedVersion) && (opt.RemovedVersion == nil || compatVer.LessThan(opt.RemovedVersion)) {
|
if compatVer.AtLeast(opt.IntroducedVersion) && (opt.RemovedVersion == nil || compatVer.LessThan(opt.RemovedVersion)) {
|
||||||
|
allowedByVersion = true
|
||||||
|
}
|
||||||
|
|
||||||
|
if allowedByFeatureGate || allowedByVersion {
|
||||||
envOpts = append(envOpts, opt.EnvOptions...)
|
envOpts = append(envOpts, opt.EnvOptions...)
|
||||||
progOpts = append(progOpts, opt.ProgramOptions...)
|
progOpts = append(progOpts, opt.ProgramOptions...)
|
||||||
declTypes = append(declTypes, opt.DeclTypes...)
|
declTypes = append(declTypes, opt.DeclTypes...)
|
||||||
|
@ -19,12 +19,13 @@ package library
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/fields"
|
"k8s.io/apimachinery/pkg/fields"
|
||||||
"k8s.io/apimachinery/pkg/labels"
|
"k8s.io/apimachinery/pkg/labels"
|
||||||
genericfeatures "k8s.io/apiserver/pkg/features"
|
genericfeatures "k8s.io/apiserver/pkg/features"
|
||||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||||
"reflect"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/google/cel-go/cel"
|
"github.com/google/cel-go/cel"
|
||||||
"github.com/google/cel-go/common/types"
|
"github.com/google/cel-go/common/types"
|
||||||
@ -198,6 +199,30 @@ import (
|
|||||||
// Examples:
|
// Examples:
|
||||||
//
|
//
|
||||||
// authorizer.group('').resource('pods').namespace('default').check('create').error()
|
// authorizer.group('').resource('pods').namespace('default').check('create').error()
|
||||||
|
//
|
||||||
|
// fieldSelector
|
||||||
|
//
|
||||||
|
// Takes a string field selector, parses it to field selector requirements, and includes it in the authorization check.
|
||||||
|
// If the field selector does not parse successfully, no field selector requirements are included in the authorization check.
|
||||||
|
// Added in Kubernetes 1.31+, Authz library version 1.
|
||||||
|
//
|
||||||
|
// <ResourceCheck>.fieldSelector(<string>) <ResourceCheck>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// authorizer.group('').resource('pods').fieldSelector('spec.nodeName=mynode').check('list').allowed()
|
||||||
|
//
|
||||||
|
// labelSelector (added in v1, Kubernetes 1.31+)
|
||||||
|
//
|
||||||
|
// Takes a string label selector, parses it to label selector requirements, and includes it in the authorization check.
|
||||||
|
// If the label selector does not parse successfully, no label selector requirements are included in the authorization check.
|
||||||
|
// Added in Kubernetes 1.31+, Authz library version 1.
|
||||||
|
//
|
||||||
|
// <ResourceCheck>.labelSelector(<string>) <ResourceCheck>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// authorizer.group('').resource('pods').labelSelector('app=example').check('list').allowed()
|
||||||
func Authz() cel.EnvOption {
|
func Authz() cel.EnvOption {
|
||||||
return cel.Lib(authzLib)
|
return cel.Lib(authzLib)
|
||||||
}
|
}
|
||||||
@ -226,12 +251,6 @@ var authzLibraryDecls = map[string][]cel.FunctionOpt{
|
|||||||
"subresource": {
|
"subresource": {
|
||||||
cel.MemberOverload("resourcecheck_subresource", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
cel.MemberOverload("resourcecheck_subresource", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
||||||
cel.BinaryBinding(resourceCheckSubresource))},
|
cel.BinaryBinding(resourceCheckSubresource))},
|
||||||
"fieldSelector": {
|
|
||||||
cel.MemberOverload("authorizer_fieldselector", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
|
||||||
cel.BinaryBinding(resourceCheckFieldSelector))},
|
|
||||||
"labelSelector": {
|
|
||||||
cel.MemberOverload("authorizer_labelselector", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
|
||||||
cel.BinaryBinding(resourceCheckLabelSelector))},
|
|
||||||
"namespace": {
|
"namespace": {
|
||||||
cel.MemberOverload("resourcecheck_namespace", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
cel.MemberOverload("resourcecheck_namespace", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
||||||
cel.BinaryBinding(resourceCheckNamespace))},
|
cel.BinaryBinding(resourceCheckNamespace))},
|
||||||
@ -269,6 +288,66 @@ func (*authz) ProgramOptions() []cel.ProgramOption {
|
|||||||
return []cel.ProgramOption{}
|
return []cel.ProgramOption{}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuthzSelectors provides a CEL function library extension for adding fieldSelector and
|
||||||
|
// labelSelector filters to authorization checks. This requires the Authz library.
|
||||||
|
// See documentation of the Authz library for use and availability of the authorizer variable.
|
||||||
|
//
|
||||||
|
// fieldSelector
|
||||||
|
//
|
||||||
|
// Takes a string field selector, parses it to field selector requirements, and includes it in the authorization check.
|
||||||
|
// If the field selector does not parse successfully, no field selector requirements are included in the authorization check.
|
||||||
|
// Added in Kubernetes 1.31+.
|
||||||
|
//
|
||||||
|
// <ResourceCheck>.fieldSelector(<string>) <ResourceCheck>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// authorizer.group('').resource('pods').fieldSelector('spec.nodeName=mynode').check('list').allowed()
|
||||||
|
//
|
||||||
|
// labelSelector
|
||||||
|
//
|
||||||
|
// Takes a string label selector, parses it to label selector requirements, and includes it in the authorization check.
|
||||||
|
// If the label selector does not parse successfully, no label selector requirements are included in the authorization check.
|
||||||
|
// Added in Kubernetes 1.31+.
|
||||||
|
//
|
||||||
|
// <ResourceCheck>.labelSelector(<string>) <ResourceCheck>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// authorizer.group('').resource('pods').labelSelector('app=example').check('list').allowed()
|
||||||
|
func AuthzSelectors() cel.EnvOption {
|
||||||
|
return cel.Lib(authzSelectorsLib)
|
||||||
|
}
|
||||||
|
|
||||||
|
var authzSelectorsLib = &authzSelectors{}
|
||||||
|
|
||||||
|
type authzSelectors struct{}
|
||||||
|
|
||||||
|
func (*authzSelectors) LibraryName() string {
|
||||||
|
return "k8s.authzSelectors"
|
||||||
|
}
|
||||||
|
|
||||||
|
var authzSelectorsLibraryDecls = map[string][]cel.FunctionOpt{
|
||||||
|
"fieldSelector": {
|
||||||
|
cel.MemberOverload("authorizer_fieldselector", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
||||||
|
cel.BinaryBinding(resourceCheckFieldSelector))},
|
||||||
|
"labelSelector": {
|
||||||
|
cel.MemberOverload("authorizer_labelselector", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
||||||
|
cel.BinaryBinding(resourceCheckLabelSelector))},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*authzSelectors) CompileOptions() []cel.EnvOption {
|
||||||
|
options := make([]cel.EnvOption, 0, len(authzSelectorsLibraryDecls))
|
||||||
|
for name, overloads := range authzSelectorsLibraryDecls {
|
||||||
|
options = append(options, cel.Function(name, overloads...))
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*authzSelectors) ProgramOptions() []cel.ProgramOption {
|
||||||
|
return []cel.ProgramOption{}
|
||||||
|
}
|
||||||
|
|
||||||
func authorizerPath(arg1, arg2 ref.Val) ref.Val {
|
func authorizerPath(arg1, arg2 ref.Val) ref.Val {
|
||||||
authz, ok := arg1.(authorizerVal)
|
authz, ok := arg1.(authorizerVal)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
@ -55,6 +55,25 @@ type CostEstimator struct {
|
|||||||
SizeEstimator checker.CostEstimator
|
SizeEstimator checker.CostEstimator
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
// shortest repeatable selector requirement that allocates a values slice is 2 characters: k,
|
||||||
|
selectorLengthToRequirementCount = float64(.5)
|
||||||
|
// the expensive parts to represent each requirement are a struct and a values slice
|
||||||
|
costPerRequirement = float64(common.ListCreateBaseCost + common.StructCreateBaseCost)
|
||||||
|
)
|
||||||
|
|
||||||
|
// a selector consists of a list of requirements held in a slice
|
||||||
|
var baseSelectorCost = checker.CostEstimate{Min: common.ListCreateBaseCost, Max: common.ListCreateBaseCost}
|
||||||
|
|
||||||
|
func selectorCostEstimate(selectorLength checker.SizeEstimate) checker.CostEstimate {
|
||||||
|
parseCost := selectorLength.MultiplyByCostFactor(common.StringTraversalCostFactor)
|
||||||
|
|
||||||
|
requirementCount := selectorLength.MultiplyByCostFactor(selectorLengthToRequirementCount)
|
||||||
|
requirementCost := requirementCount.MultiplyByCostFactor(costPerRequirement)
|
||||||
|
|
||||||
|
return baseSelectorCost.Add(parseCost).Add(requirementCost)
|
||||||
|
}
|
||||||
|
|
||||||
func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, result ref.Val) *uint64 {
|
func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, result ref.Val) *uint64 {
|
||||||
switch function {
|
switch function {
|
||||||
case "check":
|
case "check":
|
||||||
@ -66,6 +85,13 @@ func (l *CostEstimator) CallCost(function, overloadId string, args []ref.Val, re
|
|||||||
// All authorization builder and accessor functions have a nominal cost
|
// All authorization builder and accessor functions have a nominal cost
|
||||||
cost := uint64(1)
|
cost := uint64(1)
|
||||||
return &cost
|
return &cost
|
||||||
|
case "fieldSelector", "labelSelector":
|
||||||
|
// field and label selector parse is a string parse into a structured set of requirements
|
||||||
|
if len(args) >= 2 {
|
||||||
|
selectorLength := actualSize(args[1])
|
||||||
|
cost := selectorCostEstimate(checker.SizeEstimate{Min: selectorLength, Max: selectorLength})
|
||||||
|
return &cost.Max
|
||||||
|
}
|
||||||
case "isSorted", "sum", "max", "min", "indexOf", "lastIndexOf":
|
case "isSorted", "sum", "max", "min", "indexOf", "lastIndexOf":
|
||||||
var cost uint64
|
var cost uint64
|
||||||
if len(args) > 0 {
|
if len(args) > 0 {
|
||||||
@ -227,6 +253,11 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
|
|||||||
case "serviceAccount", "path", "group", "resource", "subresource", "namespace", "name", "allowed", "reason", "error", "errored":
|
case "serviceAccount", "path", "group", "resource", "subresource", "namespace", "name", "allowed", "reason", "error", "errored":
|
||||||
// All authorization builder and accessor functions have a nominal cost
|
// All authorization builder and accessor functions have a nominal cost
|
||||||
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
|
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
|
||||||
|
case "fieldSelector", "labelSelector":
|
||||||
|
// field and label selector parse is a string parse into a structured set of requirements
|
||||||
|
if len(args) == 1 {
|
||||||
|
return &checker.CallEstimate{CostEstimate: selectorCostEstimate(l.sizeEstimate(args[0]))}
|
||||||
|
}
|
||||||
case "isSorted", "sum", "max", "min", "indexOf", "lastIndexOf":
|
case "isSorted", "sum", "max", "min", "indexOf", "lastIndexOf":
|
||||||
if target != nil {
|
if target != nil {
|
||||||
// Charge 1 cost for comparing each element in the list
|
// Charge 1 cost for comparing each element in the list
|
||||||
|
@ -629,6 +629,18 @@ func TestAuthzLibrary(t *testing.T) {
|
|||||||
expectEstimatedCost: checker.CostEstimate{Min: 6, Max: 6},
|
expectEstimatedCost: checker.CostEstimate{Min: 6, Max: 6},
|
||||||
expectRuntimeCost: 6,
|
expectRuntimeCost: 6,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "fieldSelector",
|
||||||
|
expr: "authorizer.group('').resource('pods').fieldSelector('spec.nodeName=example-node-name.fully.qualified.domain.name.example.com')",
|
||||||
|
expectEstimatedCost: checker.CostEstimate{Min: 1821, Max: 1821},
|
||||||
|
expectRuntimeCost: 1821, // authorizer(1) + group(1) + resource(1) + fieldSelector(10 + ceil(71/2)*50=1800 + ceil(71*.1)=8)
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "labelSelector",
|
||||||
|
expr: "authorizer.group('').resource('pods').labelSelector('spec.nodeName=example-node-name.fully.qualified.domain.name.example.com')",
|
||||||
|
expectEstimatedCost: checker.CostEstimate{Min: 1821, Max: 1821},
|
||||||
|
expectRuntimeCost: 1821, // authorizer(1) + group(1) + resource(1) + fieldSelector(10 + ceil(71/2)*50=1800 + ceil(71*.1)=8)
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: "path check allowed",
|
name: "path check allowed",
|
||||||
expr: "authorizer.path('/healthz').check('get').allowed()",
|
expr: "authorizer.path('/healthz').check('get').allowed()",
|
||||||
@ -1064,6 +1076,7 @@ func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate
|
|||||||
Regex(),
|
Regex(),
|
||||||
Lists(),
|
Lists(),
|
||||||
Authz(),
|
Authz(),
|
||||||
|
AuthzSelectors(),
|
||||||
Quantity(),
|
Quantity(),
|
||||||
ext.Sets(),
|
ext.Sets(),
|
||||||
IP(),
|
IP(),
|
||||||
|
@ -26,7 +26,7 @@ import (
|
|||||||
|
|
||||||
func TestLibraryCompatibility(t *testing.T) {
|
func TestLibraryCompatibility(t *testing.T) {
|
||||||
var libs []map[string][]cel.FunctionOpt
|
var libs []map[string][]cel.FunctionOpt
|
||||||
libs = append(libs, authzLibraryDecls, listsLibraryDecls, regexLibraryDecls, urlLibraryDecls, quantityLibraryDecls, ipLibraryDecls, cidrLibraryDecls)
|
libs = append(libs, authzLibraryDecls, listsLibraryDecls, regexLibraryDecls, urlLibraryDecls, quantityLibraryDecls, ipLibraryDecls, cidrLibraryDecls, formatLibraryDecls, authzSelectorsLibraryDecls)
|
||||||
functionNames := sets.New[string]()
|
functionNames := sets.New[string]()
|
||||||
for _, lib := range libs {
|
for _, lib := range libs {
|
||||||
for name := range lib {
|
for name := range lib {
|
||||||
@ -49,6 +49,8 @@ func TestLibraryCompatibility(t *testing.T) {
|
|||||||
"add", "asApproximateFloat", "asInteger", "compareTo", "isGreaterThan", "isInteger", "isLessThan", "isQuantity", "quantity", "sign", "sub",
|
"add", "asApproximateFloat", "asInteger", "compareTo", "isGreaterThan", "isInteger", "isLessThan", "isQuantity", "quantity", "sign", "sub",
|
||||||
// Kubernetes <1.30>:
|
// Kubernetes <1.30>:
|
||||||
"ip", "family", "isUnspecified", "isLoopback", "isLinkLocalMulticast", "isLinkLocalUnicast", "isGlobalUnicast", "ip.isCanonical", "isIP", "cidr", "containsIP", "containsCIDR", "masked", "prefixLength", "isCIDR", "string",
|
"ip", "family", "isUnspecified", "isLoopback", "isLinkLocalMulticast", "isLinkLocalUnicast", "isGlobalUnicast", "ip.isCanonical", "isIP", "cidr", "containsIP", "containsCIDR", "masked", "prefixLength", "isCIDR", "string",
|
||||||
|
// Kubernetes <1.31>:
|
||||||
|
"fieldSelector", "labelSelector", "validate", "format.named",
|
||||||
// Kubernetes <1.??>:
|
// Kubernetes <1.??>:
|
||||||
)
|
)
|
||||||
|
|
||||||
|
292
test/integration/apiserver/cel/authorizerselector/helper.go
Normal file
292
test/integration/apiserver/cel/authorizerselector/helper.go
Normal file
@ -0,0 +1,292 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 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 authorizerselector
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
admissionregistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||||
|
resourcev1alpha2 "k8s.io/api/resource/v1alpha2"
|
||||||
|
apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1"
|
||||||
|
extclientset "k8s.io/apiextensions-apiserver/pkg/client/clientset/clientset"
|
||||||
|
apierrors "k8s.io/apimachinery/pkg/api/errors"
|
||||||
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
|
"k8s.io/apimachinery/pkg/util/sets"
|
||||||
|
"k8s.io/apiserver/pkg/cel/environment"
|
||||||
|
"k8s.io/client-go/kubernetes"
|
||||||
|
apiservertesting "k8s.io/kubernetes/cmd/kube-apiserver/app/testing"
|
||||||
|
"k8s.io/kubernetes/test/integration/framework"
|
||||||
|
"k8s.io/utils/ptr"
|
||||||
|
)
|
||||||
|
|
||||||
|
func RunAuthzSelectorsLibraryTests(t *testing.T, featureEnabled bool) {
|
||||||
|
if _, initialized := environment.AuthzSelectorsLibraryEnabled(); initialized {
|
||||||
|
// This ensures CEL environments don't get initialized during init(),
|
||||||
|
// before they can be informed by configured feature gates.
|
||||||
|
// If this check fails, uncomment the debug.PrintStack() when the authz selectors
|
||||||
|
// library is first initialized to find the culprit, and modify it to be lazily initialized on first use.
|
||||||
|
t.Fatalf("authz selector library was initialized before feature gates were finalized (possibly from an init() or package variable)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the server with the desired feature enablement
|
||||||
|
server, err := apiservertesting.StartTestServer(t, nil, []string{
|
||||||
|
fmt.Sprintf("--feature-gates=AuthorizeWithSelectors=%v", featureEnabled),
|
||||||
|
"--runtime-config=resource.k8s.io/v1alpha2=true",
|
||||||
|
}, framework.SharedEtcd())
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer server.TearDownFn()
|
||||||
|
|
||||||
|
// Ensure the authz selectors library was initialzed and saw the right feature enablement
|
||||||
|
if gotEnabled, initialized := environment.AuthzSelectorsLibraryEnabled(); !initialized {
|
||||||
|
t.Fatalf("authz selector library was not initialized during API server construction")
|
||||||
|
} else if gotEnabled != featureEnabled {
|
||||||
|
t.Fatalf("authz selector library enabled=%v, expected %v", gotEnabled, featureEnabled)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to create API objects using the fieldSelector and labelSelector authorizer functions,
|
||||||
|
// and ensure they are only allowed when the feature is enabled.
|
||||||
|
|
||||||
|
c, err := kubernetes.NewForConfig(server.ClientConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create clientset: %v", err)
|
||||||
|
}
|
||||||
|
crdClient, err := extclientset.NewForConfig(server.ClientConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to create clientset: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
boolFieldSelectorExpression := `type(authorizer.group('').resource('').fieldSelector('')) == string`
|
||||||
|
stringFieldSelectorExpression := boolFieldSelectorExpression + ` ? 'yes' : 'no'`
|
||||||
|
fieldSelectorErrorSubstring := `undeclared reference to 'fieldSelector'`
|
||||||
|
|
||||||
|
testcases := []struct {
|
||||||
|
name string
|
||||||
|
createObject func() error
|
||||||
|
expectErrorsWhenEnabled []*regexp.Regexp
|
||||||
|
expectErrorsWhenDisabled []*regexp.Regexp
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "ValidatingAdmissionPolicy",
|
||||||
|
createObject: func() error {
|
||||||
|
obj := &admissionregistrationv1.ValidatingAdmissionPolicy{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test-with-variables"},
|
||||||
|
Spec: admissionregistrationv1.ValidatingAdmissionPolicySpec{
|
||||||
|
MatchConstraints: &admissionregistrationv1.MatchResources{
|
||||||
|
ResourceRules: []admissionregistrationv1.NamedRuleWithOperations{{
|
||||||
|
RuleWithOperations: admissionregistrationv1.RuleWithOperations{
|
||||||
|
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
|
||||||
|
Rule: admissionregistrationv1.Rule{APIGroups: []string{"example.com"}, APIVersions: []string{"*"}, Resources: []string{"*"}}}}}},
|
||||||
|
Validations: []admissionregistrationv1.Validation{{
|
||||||
|
Expression: boolFieldSelectorExpression,
|
||||||
|
MessageExpression: stringFieldSelectorExpression}},
|
||||||
|
AuditAnnotations: []admissionregistrationv1.AuditAnnotation{{Key: "test", ValueExpression: stringFieldSelectorExpression}},
|
||||||
|
MatchConditions: []admissionregistrationv1.MatchCondition{{Name: "test", Expression: boolFieldSelectorExpression}},
|
||||||
|
Variables: []admissionregistrationv1.Variable{{Name: "test", Expression: boolFieldSelectorExpression}}}}
|
||||||
|
_, err := c.AdmissionregistrationV1().ValidatingAdmissionPolicies().Create(context.TODO(), obj, metav1.CreateOptions{})
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
expectErrorsWhenEnabled: []*regexp.Regexp{
|
||||||
|
// authorizer is not available to messageExpression
|
||||||
|
regexp.MustCompile(`spec\.validations\[0\]\.messageExpression:.*undeclared reference to 'authorizer'`),
|
||||||
|
},
|
||||||
|
expectErrorsWhenDisabled: []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`spec\.validations\[0\]\.expression:.*` + fieldSelectorErrorSubstring),
|
||||||
|
// authorizer is not available to messageExpression
|
||||||
|
regexp.MustCompile(`spec\.validations\[0\]\.messageExpression:.*undeclared reference to 'authorizer'`),
|
||||||
|
regexp.MustCompile(`spec\.auditAnnotations\[0\]\.valueExpression:.*` + fieldSelectorErrorSubstring),
|
||||||
|
regexp.MustCompile(`spec\.matchConditions\[0\]\.expression:.*` + fieldSelectorErrorSubstring),
|
||||||
|
regexp.MustCompile(`spec\.variables\[0\]\.expression:.*` + fieldSelectorErrorSubstring),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ValidatingWebhookConfiguration",
|
||||||
|
createObject: func() error {
|
||||||
|
obj := &admissionregistrationv1.ValidatingWebhookConfiguration{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test"},
|
||||||
|
Webhooks: []admissionregistrationv1.ValidatingWebhook{{
|
||||||
|
Name: "test.example.com",
|
||||||
|
ClientConfig: admissionregistrationv1.WebhookClientConfig{URL: ptr.To("https://127.0.0.1")},
|
||||||
|
AdmissionReviewVersions: []string{"v1"},
|
||||||
|
SideEffects: ptr.To(admissionregistrationv1.SideEffectClassNone),
|
||||||
|
Rules: []admissionregistrationv1.RuleWithOperations{{
|
||||||
|
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
|
||||||
|
Rule: admissionregistrationv1.Rule{APIGroups: []string{"example.com"}, APIVersions: []string{"*"}, Resources: []string{"*"}}}},
|
||||||
|
MatchConditions: []admissionregistrationv1.MatchCondition{{Name: "test", Expression: boolFieldSelectorExpression}}}}}
|
||||||
|
_, err := c.AdmissionregistrationV1().ValidatingWebhookConfigurations().Create(context.TODO(), obj, metav1.CreateOptions{})
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
expectErrorsWhenDisabled: []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`webhooks\[0\]\.matchConditions\[0\]\.expression:.*` + fieldSelectorErrorSubstring),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "MutatingWebhookConfiguration",
|
||||||
|
createObject: func() error {
|
||||||
|
obj := &admissionregistrationv1.MutatingWebhookConfiguration{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test"},
|
||||||
|
Webhooks: []admissionregistrationv1.MutatingWebhook{{
|
||||||
|
Name: "test.example.com",
|
||||||
|
ClientConfig: admissionregistrationv1.WebhookClientConfig{URL: ptr.To("https://127.0.0.1")},
|
||||||
|
AdmissionReviewVersions: []string{"v1"},
|
||||||
|
SideEffects: ptr.To(admissionregistrationv1.SideEffectClassNone),
|
||||||
|
Rules: []admissionregistrationv1.RuleWithOperations{{
|
||||||
|
Operations: []admissionregistrationv1.OperationType{admissionregistrationv1.OperationAll},
|
||||||
|
Rule: admissionregistrationv1.Rule{APIGroups: []string{"example.com"}, APIVersions: []string{"*"}, Resources: []string{"*"}}}},
|
||||||
|
MatchConditions: []admissionregistrationv1.MatchCondition{{Name: "test", Expression: boolFieldSelectorExpression}}}}}
|
||||||
|
_, err := c.AdmissionregistrationV1().MutatingWebhookConfigurations().Create(context.TODO(), obj, metav1.CreateOptions{})
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
expectErrorsWhenDisabled: []*regexp.Regexp{
|
||||||
|
regexp.MustCompile(`webhooks\[0\]\.matchConditions\[0\]\.expression:.*` + fieldSelectorErrorSubstring),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "ResourceClaimParameters",
|
||||||
|
createObject: func() error {
|
||||||
|
obj := &resourcev1alpha2.ResourceClaimParameters{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "test"},
|
||||||
|
DriverRequests: []resourcev1alpha2.DriverRequests{{
|
||||||
|
DriverName: "example.com",
|
||||||
|
Requests: []resourcev1alpha2.ResourceRequest{{
|
||||||
|
ResourceRequestModel: resourcev1alpha2.ResourceRequestModel{
|
||||||
|
NamedResources: &resourcev1alpha2.NamedResourcesRequest{Selector: boolFieldSelectorExpression}}}}}}}
|
||||||
|
_, err := c.ResourceV1alpha2().ResourceClaimParameters("default").Create(context.TODO(), obj, metav1.CreateOptions{})
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
// authorizer is not available to resource APIs
|
||||||
|
expectErrorsWhenEnabled: []*regexp.Regexp{regexp.MustCompile(`driverRequests\[0\]\.requests\[0\]\.namedResources\.selector:.*undeclared reference to 'authorizer'`)},
|
||||||
|
expectErrorsWhenDisabled: []*regexp.Regexp{regexp.MustCompile(`driverRequests\[0\]\.requests\[0\]\.namedResources\.selector:.*undeclared reference to 'authorizer'`)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CustomResourceDefinition - rule",
|
||||||
|
createObject: func() error {
|
||||||
|
obj := &apiextensionsv1.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{Name: "crontabs.apis.example.com"},
|
||||||
|
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
|
||||||
|
Group: "apis.example.com",
|
||||||
|
Scope: apiextensionsv1.NamespaceScoped,
|
||||||
|
Names: apiextensionsv1.CustomResourceDefinitionNames{Plural: "crontabs", Singular: "crontab", Kind: "CronTab", ListKind: "CronTabList"},
|
||||||
|
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{
|
||||||
|
Name: "v1beta1",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
Schema: &apiextensionsv1.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensionsv1.JSONSchemaProps{
|
||||||
|
"spec": {
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensionsv1.ValidationRules{{Rule: boolFieldSelectorExpression}}}}}}}}}}
|
||||||
|
_, err := crdClient.ApiextensionsV1().CustomResourceDefinitions().Create(context.TODO(), obj, metav1.CreateOptions{})
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
// authorizer is not available to CRD validation
|
||||||
|
expectErrorsWhenEnabled: []*regexp.Regexp{regexp.MustCompile(`x-kubernetes-validations\[0\]\.rule:.*undeclared reference to 'authorizer'`)},
|
||||||
|
expectErrorsWhenDisabled: []*regexp.Regexp{regexp.MustCompile(`x-kubernetes-validations\[0\]\.rule:.*undeclared reference to 'authorizer'`)},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "CustomResourceDefinition - messageExpression",
|
||||||
|
createObject: func() error {
|
||||||
|
obj := &apiextensionsv1.CustomResourceDefinition{
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: "crontabs.apis.example.com"},
|
||||||
|
Spec: apiextensionsv1.CustomResourceDefinitionSpec{
|
||||||
|
Group: "apis.example.com",
|
||||||
|
Scope: apiextensionsv1.NamespaceScoped,
|
||||||
|
Names: apiextensionsv1.CustomResourceDefinitionNames{Plural: "crontabs", Singular: "crontab", Kind: "CronTab", ListKind: "CronTabList"},
|
||||||
|
Versions: []apiextensionsv1.CustomResourceDefinitionVersion{{
|
||||||
|
Name: "v1beta1",
|
||||||
|
Served: true,
|
||||||
|
Storage: true,
|
||||||
|
Schema: &apiextensionsv1.CustomResourceValidation{
|
||||||
|
OpenAPIV3Schema: &apiextensionsv1.JSONSchemaProps{
|
||||||
|
Type: "object",
|
||||||
|
Properties: map[string]apiextensionsv1.JSONSchemaProps{
|
||||||
|
"spec": {
|
||||||
|
Type: "object",
|
||||||
|
XValidations: apiextensionsv1.ValidationRules{{Rule: `self == oldSelf`, MessageExpression: stringFieldSelectorExpression}}}}}}}}}}
|
||||||
|
_, err := crdClient.ApiextensionsV1().CustomResourceDefinitions().Create(context.TODO(), obj, metav1.CreateOptions{})
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
// authorizer is not available to CRD validation
|
||||||
|
expectErrorsWhenEnabled: []*regexp.Regexp{regexp.MustCompile(`x-kubernetes-validations\[0\]\.messageExpression:.*undeclared reference to 'authorizer'`)},
|
||||||
|
expectErrorsWhenDisabled: []*regexp.Regexp{regexp.MustCompile(`x-kubernetes-validations\[0\]\.messageExpression:.*undeclared reference to 'authorizer'`)},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, tc := range testcases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
err := tc.createObject()
|
||||||
|
|
||||||
|
var expectedErrors []*regexp.Regexp
|
||||||
|
if featureEnabled {
|
||||||
|
expectedErrors = tc.expectErrorsWhenEnabled
|
||||||
|
} else {
|
||||||
|
expectedErrors = tc.expectErrorsWhenDisabled
|
||||||
|
}
|
||||||
|
|
||||||
|
switch {
|
||||||
|
case len(expectedErrors) == 0 && err == nil:
|
||||||
|
// success
|
||||||
|
case len(expectedErrors) == 0 && err != nil:
|
||||||
|
t.Fatalf("expected success, got error:\n%s", strings.Join(sets.List(getCauses(t, err)), "\n\n"))
|
||||||
|
case len(expectedErrors) > 0 && err == nil:
|
||||||
|
t.Fatalf("expected error, got success")
|
||||||
|
case len(expectedErrors) > 0 && err != nil:
|
||||||
|
// make sure errors match expectations
|
||||||
|
actualCauses := getCauses(t, err)
|
||||||
|
for _, expectCause := range expectedErrors {
|
||||||
|
found := false
|
||||||
|
for _, cause := range actualCauses.UnsortedList() {
|
||||||
|
if expectCause.MatchString(cause) {
|
||||||
|
actualCauses.Delete(cause)
|
||||||
|
found = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
t.Errorf("missing error matching %s", expectCause)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(actualCauses) > 0 {
|
||||||
|
t.Errorf("unexpected errors:\n%s", strings.Join(sets.List(actualCauses), "\n\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getCauses(t *testing.T, err error) sets.Set[string] {
|
||||||
|
t.Helper()
|
||||||
|
status, ok := err.(apierrors.APIStatus)
|
||||||
|
if !ok {
|
||||||
|
t.Fatalf("expected API status error, got %#v", err)
|
||||||
|
}
|
||||||
|
if len(status.Status().Details.Causes) == 0 {
|
||||||
|
t.Fatalf("expected API status error with causes, got %#v", err)
|
||||||
|
}
|
||||||
|
causes := sets.New[string]()
|
||||||
|
for _, cause := range status.Status().Details.Causes {
|
||||||
|
causes.Insert(cause.Field + ": " + cause.Message)
|
||||||
|
}
|
||||||
|
return causes
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 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 selectordisabled
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/test/integration/apiserver/cel/authorizerselector"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestAuthzSelectorsLibraryDisabled ensures that the authzselectors library feature disablement works properly.
|
||||||
|
// CEL envs and compilers cached per process mean this must be the only test in this package.
|
||||||
|
func TestAuthzSelectorsLibraryDisabled(t *testing.T) {
|
||||||
|
authorizerselector.RunAuthzSelectorsLibraryTests(t, false)
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 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 selectordisabled
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/test/integration/framework"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
framework.EtcdMain(m.Run)
|
||||||
|
}
|
@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 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 selectorenabled
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/test/integration/apiserver/cel/authorizerselector"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestAuthzSelectorsLibraryEnabled ensures that the authzselectors library feature enablement works properly.
|
||||||
|
// CEL envs and compilers cached per process mean this must be the only test in this package.
|
||||||
|
func TestAuthzSelectorsLibraryEnabled(t *testing.T) {
|
||||||
|
authorizerselector.RunAuthzSelectorsLibraryTests(t, true)
|
||||||
|
}
|
@ -0,0 +1,27 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2024 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 selectorenabled
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"k8s.io/kubernetes/test/integration/framework"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMain(m *testing.M) {
|
||||||
|
framework.EtcdMain(m.Run)
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user