mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-21 02:41:25 +00:00
Implement secondary authz
This commit is contained in:
parent
b6d102d634
commit
7bbda746fe
@ -186,10 +186,14 @@ type Validation struct {
|
|||||||
// ref: https://github.com/google/cel-spec
|
// ref: https://github.com/google/cel-spec
|
||||||
// CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables:
|
// CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables:
|
||||||
//
|
//
|
||||||
//'object' - The object from the incoming request. The value is null for DELETE requests.
|
// - 'object' - The object from the incoming request. The value is null for DELETE requests.
|
||||||
//'oldObject' - The existing object. The value is null for CREATE requests.
|
// - 'oldObject' - The existing object. The value is null for CREATE requests.
|
||||||
//'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)).
|
// - 'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)).
|
||||||
//'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.
|
// - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.
|
||||||
|
// - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.
|
||||||
|
// See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz
|
||||||
|
// - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the
|
||||||
|
// request resource.
|
||||||
//
|
//
|
||||||
// The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the
|
// The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the
|
||||||
// object. No other metadata properties are accessible.
|
// object. No other metadata properties are accessible.
|
||||||
|
@ -18,10 +18,11 @@ package validation
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
|
|
||||||
"regexp"
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy"
|
||||||
|
|
||||||
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
genericvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||||
"k8s.io/apimachinery/pkg/api/validation/path"
|
"k8s.io/apimachinery/pkg/api/validation/path"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
@ -32,6 +33,7 @@ import (
|
|||||||
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
|
plugincel "k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||||
"k8s.io/apiserver/pkg/cel"
|
"k8s.io/apiserver/pkg/cel"
|
||||||
"k8s.io/apiserver/pkg/util/webhook"
|
"k8s.io/apiserver/pkg/util/webhook"
|
||||||
|
|
||||||
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
"k8s.io/kubernetes/pkg/apis/admissionregistration"
|
||||||
admissionregistrationv1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1"
|
admissionregistrationv1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1"
|
||||||
admissionregistrationv1beta1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1beta1"
|
admissionregistrationv1beta1 "k8s.io/kubernetes/pkg/apis/admissionregistration/v1beta1"
|
||||||
@ -738,7 +740,7 @@ func validateValidation(v *admissionregistration.Validation, paramKind *admissio
|
|||||||
Expression: trimmedExpression,
|
Expression: trimmedExpression,
|
||||||
Message: v.Message,
|
Message: v.Message,
|
||||||
Reason: v.Reason,
|
Reason: v.Reason,
|
||||||
}, paramKind != nil)
|
}, plugincel.OptionalVariableDeclarations{HasParams: paramKind != nil, HasAuthorizer: true})
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
switch result.Error.Type {
|
switch result.Error.Type {
|
||||||
case cel.ErrorTypeRequired:
|
case cel.ErrorTypeRequired:
|
||||||
|
@ -140,10 +140,14 @@ type Validation struct {
|
|||||||
// ref: https://github.com/google/cel-spec
|
// ref: https://github.com/google/cel-spec
|
||||||
// CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables:
|
// CEL expressions have access to the contents of the Admission request/response, organized into CEL variables as well as some other useful variables:
|
||||||
//
|
//
|
||||||
//'object' - The object from the incoming request. The value is null for DELETE requests.
|
// - 'object' - The object from the incoming request. The value is null for DELETE requests.
|
||||||
//'oldObject' - The existing object. The value is null for CREATE requests.
|
// - 'oldObject' - The existing object. The value is null for CREATE requests.
|
||||||
//'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)).
|
// - 'request' - Attributes of the admission request([ref](/pkg/apis/admission/types.go#AdmissionRequest)).
|
||||||
//'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.
|
// - 'params' - Parameter resource referred to by the policy binding being evaluated. Only populated if the policy has a ParamKind.
|
||||||
|
// - 'authorizer' - A CEL Authorizer. May be used to perform authorization checks for the principal (user or service account) of the request.
|
||||||
|
// See https://pkg.go.dev/k8s.io/apiserver/pkg/cel/library#Authz
|
||||||
|
// - 'authorizer.requestResource' - A CEL ResourceCheck constructed from the 'authorizer' and configured with the
|
||||||
|
// request resource.
|
||||||
//
|
//
|
||||||
// The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the
|
// The `apiVersion`, `kind`, `metadata.name` and `metadata.generateName` are always accessible from the root of the
|
||||||
// object. No other metadata properties are accessible.
|
// object. No other metadata properties are accessible.
|
||||||
|
@ -1751,6 +1751,14 @@ func TestValidationExpressions(t *testing.T) {
|
|||||||
"oldSelf == self",
|
"oldSelf == self",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{name: "authorizer is not supported for CRD Validation Rules",
|
||||||
|
obj: []interface{}{},
|
||||||
|
oldObj: []interface{}{},
|
||||||
|
schema: objectTypePtr(map[string]schema.Structural{}),
|
||||||
|
errors: map[string]string{
|
||||||
|
"authorizer.path('/healthz').check('get').isAllowed()": "undeclared reference to 'authorizer'",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for i := range tests {
|
for i := range tests {
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package cel
|
package cel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
"github.com/google/cel-go/cel"
|
"github.com/google/cel-go/cel"
|
||||||
@ -26,43 +27,35 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
ObjectVarName = "object"
|
ObjectVarName = "object"
|
||||||
OldObjectVarName = "oldObject"
|
OldObjectVarName = "oldObject"
|
||||||
ParamsVarName = "params"
|
ParamsVarName = "params"
|
||||||
RequestVarName = "request"
|
RequestVarName = "request"
|
||||||
|
AuthorizerVarName = "authorizer"
|
||||||
|
RequestResourceAuthorizerVarName = "authorizer.requestResource"
|
||||||
|
|
||||||
checkFrequency = 100
|
checkFrequency = 100
|
||||||
)
|
)
|
||||||
|
|
||||||
type envs struct {
|
|
||||||
noParams *cel.Env
|
|
||||||
withParams *cel.Env
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
var (
|
||||||
initEnvsOnce sync.Once
|
initEnvsOnce sync.Once
|
||||||
initEnvs *envs
|
initEnvs envs
|
||||||
initEnvsErr error
|
initEnvsErr error
|
||||||
)
|
)
|
||||||
|
|
||||||
func getEnvs() (*envs, error) {
|
func getEnvs() (envs, error) {
|
||||||
initEnvsOnce.Do(func() {
|
initEnvsOnce.Do(func() {
|
||||||
base, err := buildBaseEnv()
|
requiredVarsEnv, err := buildRequiredVarsEnv()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
initEnvsErr = err
|
initEnvsErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
noParams, err := buildNoParamsEnv(base)
|
|
||||||
|
initEnvs, err = buildWithOptionalVarsEnvs(requiredVarsEnv)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
initEnvsErr = err
|
initEnvsErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
withParams, err := buildWithParamsEnv(noParams)
|
|
||||||
if err != nil {
|
|
||||||
initEnvsErr = err
|
|
||||||
return
|
|
||||||
}
|
|
||||||
initEnvs = &envs{noParams: noParams, withParams: withParams}
|
|
||||||
})
|
})
|
||||||
return initEnvs, initEnvsErr
|
return initEnvs, initEnvsErr
|
||||||
}
|
}
|
||||||
@ -81,7 +74,11 @@ func buildBaseEnv() (*cel.Env, error) {
|
|||||||
return cel.NewEnv(opts...)
|
return cel.NewEnv(opts...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildNoParamsEnv(baseEnv *cel.Env) (*cel.Env, error) {
|
func buildRequiredVarsEnv() (*cel.Env, error) {
|
||||||
|
baseEnv, err := buildBaseEnv()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
var propDecls []cel.EnvOption
|
var propDecls []cel.EnvOption
|
||||||
reg := apiservercel.NewRegistry(baseEnv)
|
reg := apiservercel.NewRegistry(baseEnv)
|
||||||
|
|
||||||
@ -109,8 +106,33 @@ func buildNoParamsEnv(baseEnv *cel.Env) (*cel.Env, error) {
|
|||||||
return env, nil
|
return env, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func buildWithParamsEnv(noParams *cel.Env) (*cel.Env, error) {
|
type envs map[OptionalVariableDeclarations]*cel.Env
|
||||||
return noParams.Extend(cel.Variable(ParamsVarName, cel.DynType))
|
|
||||||
|
func buildEnvWithVars(baseVarsEnv *cel.Env, options OptionalVariableDeclarations) (*cel.Env, error) {
|
||||||
|
var opts []cel.EnvOption
|
||||||
|
if options.HasParams {
|
||||||
|
opts = append(opts, cel.Variable(ParamsVarName, cel.DynType))
|
||||||
|
}
|
||||||
|
if options.HasAuthorizer {
|
||||||
|
opts = append(opts, cel.Variable(AuthorizerVarName, library.AuthorizerType))
|
||||||
|
opts = append(opts, cel.Variable(RequestResourceAuthorizerVarName, library.ResourceCheckType))
|
||||||
|
}
|
||||||
|
return baseVarsEnv.Extend(opts...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func buildWithOptionalVarsEnvs(requiredVarsEnv *cel.Env) (envs, error) {
|
||||||
|
envs := make(envs, 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} {
|
||||||
|
opts := OptionalVariableDeclarations{HasParams: hasParams, HasAuthorizer: hasAuthorizer}
|
||||||
|
env, err := buildEnvWithVars(requiredVarsEnv, opts)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
envs[opts] = env
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return envs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// buildRequestType generates a DeclType for AdmissionRequest. This may be replaced with a utility that
|
// buildRequestType generates a DeclType for AdmissionRequest. This may be replaced with a utility that
|
||||||
@ -168,7 +190,7 @@ type CompilationResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// CompileCELExpression returns a compiled CEL expression.
|
// CompileCELExpression returns a compiled CEL expression.
|
||||||
func CompileCELExpression(expressionAccessor ExpressionAccessor, hasParams bool) CompilationResult {
|
func CompileCELExpression(expressionAccessor ExpressionAccessor, optionalVars OptionalVariableDeclarations) CompilationResult {
|
||||||
var env *cel.Env
|
var env *cel.Env
|
||||||
envs, err := getEnvs()
|
envs, err := getEnvs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -180,10 +202,15 @@ func CompileCELExpression(expressionAccessor ExpressionAccessor, hasParams bool)
|
|||||||
ExpressionAccessor: expressionAccessor,
|
ExpressionAccessor: expressionAccessor,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if hasParams {
|
env, ok := envs[optionalVars]
|
||||||
env = envs.withParams
|
if !ok {
|
||||||
} else {
|
return CompilationResult{
|
||||||
env = envs.noParams
|
Error: &apiservercel.Error{
|
||||||
|
Type: apiservercel.ErrorTypeInvalid,
|
||||||
|
Detail: fmt.Sprintf("compiler initialization failed: failed to load environment for %v", optionalVars),
|
||||||
|
},
|
||||||
|
ExpressionAccessor: expressionAccessor,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ast, issues := env.Compile(expressionAccessor.GetExpression())
|
ast, issues := env.Compile(expressionAccessor.GetExpression())
|
||||||
|
@ -26,6 +26,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
|||||||
name string
|
name string
|
||||||
expressions []string
|
expressions []string
|
||||||
hasParams bool
|
hasParams bool
|
||||||
|
hasAuthorizer bool
|
||||||
errorExpressions map[string]string
|
errorExpressions map[string]string
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@ -99,6 +100,19 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
|||||||
"request.userInfo.foo5 == 'nope'": "undefined field 'foo5'",
|
"request.userInfo.foo5 == 'nope'": "undefined field 'foo5'",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "with authorizer",
|
||||||
|
hasAuthorizer: true,
|
||||||
|
expressions: []string{
|
||||||
|
"authorizer.group('') != null",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "without authorizer",
|
||||||
|
errorExpressions: map[string]string{
|
||||||
|
"authorizer.group('') != null": "undeclared reference to 'authorizer'",
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
@ -106,7 +120,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
|||||||
for _, expr := range tc.expressions {
|
for _, expr := range tc.expressions {
|
||||||
result := CompileCELExpression(&fakeExpressionAccessor{
|
result := CompileCELExpression(&fakeExpressionAccessor{
|
||||||
expr,
|
expr,
|
||||||
}, tc.hasParams)
|
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: true})
|
||||||
if result.Error != nil {
|
if result.Error != nil {
|
||||||
t.Errorf("Unexpected error: %v", result.Error)
|
t.Errorf("Unexpected error: %v", result.Error)
|
||||||
}
|
}
|
||||||
@ -114,7 +128,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
|||||||
for expr, expectErr := range tc.errorExpressions {
|
for expr, expectErr := range tc.errorExpressions {
|
||||||
result := CompileCELExpression(&fakeExpressionAccessor{
|
result := CompileCELExpression(&fakeExpressionAccessor{
|
||||||
expr,
|
expr,
|
||||||
}, tc.hasParams)
|
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer})
|
||||||
if result.Error == nil {
|
if result.Error == nil {
|
||||||
t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
|
t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
|
||||||
continue
|
continue
|
||||||
|
@ -31,6 +31,7 @@ import (
|
|||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||||
|
"k8s.io/apiserver/pkg/cel/library"
|
||||||
)
|
)
|
||||||
|
|
||||||
// filterCompiler implement the interface FilterCompiler.
|
// filterCompiler implement the interface FilterCompiler.
|
||||||
@ -42,7 +43,7 @@ func NewFilterCompiler() FilterCompiler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type evaluationActivation struct {
|
type evaluationActivation struct {
|
||||||
object, oldObject, params, request interface{}
|
object, oldObject, params, request, authorizer, requestResourceAuthorizer interface{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ResolveName returns a value from the activation by qualified name, or false if the name
|
// ResolveName returns a value from the activation by qualified name, or false if the name
|
||||||
@ -54,9 +55,13 @@ func (a *evaluationActivation) ResolveName(name string) (interface{}, bool) {
|
|||||||
case OldObjectVarName:
|
case OldObjectVarName:
|
||||||
return a.oldObject, true
|
return a.oldObject, true
|
||||||
case ParamsVarName:
|
case ParamsVarName:
|
||||||
return a.params, true
|
return a.params, true // params may be null
|
||||||
case RequestVarName:
|
case RequestVarName:
|
||||||
return a.request, true
|
return a.request, true
|
||||||
|
case AuthorizerVarName:
|
||||||
|
return a.authorizer, a.authorizer != nil
|
||||||
|
case RequestResourceAuthorizerVarName:
|
||||||
|
return a.requestResourceAuthorizer, a.requestResourceAuthorizer != nil
|
||||||
default:
|
default:
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
@ -69,13 +74,13 @@ func (a *evaluationActivation) Parent() interpreter.Activation {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Compile compiles the cel expressions defined in the ExpressionAccessors into a Filter
|
// Compile compiles the cel expressions defined in the ExpressionAccessors into a Filter
|
||||||
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, hasParam bool) Filter {
|
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations) Filter {
|
||||||
if len(expressionAccessors) == 0 {
|
if len(expressionAccessors) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
compilationResults := make([]CompilationResult, len(expressionAccessors))
|
compilationResults := make([]CompilationResult, len(expressionAccessors))
|
||||||
for i, expressionAccessor := range expressionAccessors {
|
for i, expressionAccessor := range expressionAccessors {
|
||||||
compilationResults[i] = CompileCELExpression(expressionAccessor, hasParam)
|
compilationResults[i] = CompileCELExpression(expressionAccessor, options)
|
||||||
}
|
}
|
||||||
return NewFilter(compilationResults)
|
return NewFilter(compilationResults)
|
||||||
}
|
}
|
||||||
@ -113,9 +118,9 @@ func objectToResolveVal(r runtime.Object) (interface{}, error) {
|
|||||||
return v.Object, nil
|
return v.Object, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Evaluate evaluates the compiled CEL expressions converting them into CELEvaluations
|
// ForInput evaluates the compiled CEL expressions converting them into CELEvaluations
|
||||||
// errors per evaluation are returned on the Evaluation object
|
// errors per evaluation are returned on the Evaluation object
|
||||||
func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, request *admissionv1.AdmissionRequest) ([]EvaluationResult, error) {
|
func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs OptionalVariableBindings) ([]EvaluationResult, error) {
|
||||||
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
|
// TODO: replace unstructured with ref.Val for CEL variables when native type support is available
|
||||||
evaluations := make([]EvaluationResult, len(f.compilationResults))
|
evaluations := make([]EvaluationResult, len(f.compilationResults))
|
||||||
var err error
|
var err error
|
||||||
@ -128,9 +133,17 @@ func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, versionedP
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
paramsVal, err := objectToResolveVal(versionedParams)
|
var paramsVal, authorizerVal, requestResourceAuthorizerVal any
|
||||||
if err != nil {
|
if inputs.VersionedParams != nil {
|
||||||
return nil, err
|
paramsVal, err = objectToResolveVal(inputs.VersionedParams)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if inputs.Authorizer != nil {
|
||||||
|
authorizerVal = library.NewAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer)
|
||||||
|
requestResourceAuthorizerVal = library.NewResourceAuthorizerVal(versionedAttr.GetUserInfo(), inputs.Authorizer, versionedAttr)
|
||||||
}
|
}
|
||||||
|
|
||||||
requestVal, err := convertObjectToUnstructured(request)
|
requestVal, err := convertObjectToUnstructured(request)
|
||||||
@ -138,10 +151,12 @@ func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, versionedP
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
va := &evaluationActivation{
|
va := &evaluationActivation{
|
||||||
object: objectVal,
|
object: objectVal,
|
||||||
oldObject: oldObjectVal,
|
oldObject: oldObjectVal,
|
||||||
params: paramsVal,
|
params: paramsVal,
|
||||||
request: requestVal.Object,
|
request: requestVal.Object,
|
||||||
|
authorizer: authorizerVal,
|
||||||
|
requestResourceAuthorizer: requestResourceAuthorizerVal,
|
||||||
}
|
}
|
||||||
|
|
||||||
for i, compilationResult := range f.compilationResults {
|
for i, compilationResult := range f.compilationResults {
|
||||||
|
@ -17,12 +17,18 @@ limitations under the License.
|
|||||||
package cel
|
package cel
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
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/apiserver/pkg/authentication/user"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||||
|
|
||||||
corev1 "k8s.io/api/core/v1"
|
corev1 "k8s.io/api/core/v1"
|
||||||
@ -81,7 +87,7 @@ func TestCompile(t *testing.T) {
|
|||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
var c filterCompiler
|
var c filterCompiler
|
||||||
e := c.Compile(tc.validation, false)
|
e := c.Compile(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false})
|
||||||
if e == nil {
|
if e == nil {
|
||||||
t.Fatalf("unexpected nil validator")
|
t.Fatalf("unexpected nil validator")
|
||||||
}
|
}
|
||||||
@ -144,6 +150,7 @@ func TestFilter(t *testing.T) {
|
|||||||
validations []ExpressionAccessor
|
validations []ExpressionAccessor
|
||||||
results []EvaluationResult
|
results []EvaluationResult
|
||||||
hasParamKind bool
|
hasParamKind bool
|
||||||
|
authorizer authorizer.Authorizer
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "valid syntax for object",
|
name: "valid syntax for object",
|
||||||
@ -417,12 +424,204 @@ func TestFilter(t *testing.T) {
|
|||||||
hasParamKind: true,
|
hasParamKind: true,
|
||||||
params: runtime.Object(nilUnstructured),
|
params: runtime.Object(nilUnstructured),
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: "test authorizer allow resource check",
|
||||||
|
validations: []ExpressionAccessor{
|
||||||
|
&condition{
|
||||||
|
Expression: "authorizer.group('').resource('endpoints').check('create').allowed()",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attributes: newValidAttribute(&podObject, false),
|
||||||
|
results: []EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
|
||||||
|
ResourceRequest: true,
|
||||||
|
Resource: "endpoints",
|
||||||
|
Verb: "create",
|
||||||
|
APIVersion: "*",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test authorizer allow resource check with all fields",
|
||||||
|
validations: []ExpressionAccessor{
|
||||||
|
&condition{
|
||||||
|
Expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attributes: newValidAttribute(&podObject, false),
|
||||||
|
results: []EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
|
||||||
|
ResourceRequest: true,
|
||||||
|
APIGroup: "apps",
|
||||||
|
Resource: "deployments",
|
||||||
|
Subresource: "status",
|
||||||
|
Namespace: "test",
|
||||||
|
Name: "backend",
|
||||||
|
Verb: "create",
|
||||||
|
APIVersion: "*",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test authorizer not allowed resource check one incorrect field",
|
||||||
|
validations: []ExpressionAccessor{
|
||||||
|
&condition{
|
||||||
|
|
||||||
|
Expression: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attributes: newValidAttribute(&podObject, false),
|
||||||
|
results: []EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.False,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
|
||||||
|
ResourceRequest: true,
|
||||||
|
APIGroup: "apps",
|
||||||
|
Resource: "deployments-xxxx",
|
||||||
|
Subresource: "status",
|
||||||
|
Namespace: "test",
|
||||||
|
Name: "backend",
|
||||||
|
Verb: "create",
|
||||||
|
APIVersion: "*",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test authorizer reason",
|
||||||
|
validations: []ExpressionAccessor{
|
||||||
|
&condition{
|
||||||
|
Expression: "authorizer.group('').resource('endpoints').check('create').reason() == 'fake reason'",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attributes: newValidAttribute(&podObject, false),
|
||||||
|
results: []EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authorizer: denyAll,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test authorizer allow path check",
|
||||||
|
validations: []ExpressionAccessor{
|
||||||
|
&condition{
|
||||||
|
Expression: "authorizer.path('/healthz').check('get').allowed()",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attributes: newValidAttribute(&podObject, false),
|
||||||
|
results: []EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
|
||||||
|
Path: "/healthz",
|
||||||
|
Verb: "get",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test authorizer decision is denied path check",
|
||||||
|
validations: []ExpressionAccessor{
|
||||||
|
&condition{
|
||||||
|
Expression: "authorizer.path('/healthz').check('get').allowed() == false",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attributes: newValidAttribute(&podObject, false),
|
||||||
|
results: []EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authorizer: denyAll,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test request resource authorizer allow check",
|
||||||
|
validations: []ExpressionAccessor{
|
||||||
|
&condition{
|
||||||
|
Expression: "authorizer.requestResource.check('custom-verb').allowed()",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attributes: endpointCreateAttributes(),
|
||||||
|
results: []EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
|
||||||
|
ResourceRequest: true,
|
||||||
|
APIGroup: "",
|
||||||
|
Resource: "endpoints",
|
||||||
|
Subresource: "",
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "endpoints1",
|
||||||
|
Verb: "custom-verb",
|
||||||
|
APIVersion: "*",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test subresource request resource authorizer allow check",
|
||||||
|
validations: []ExpressionAccessor{
|
||||||
|
&condition{
|
||||||
|
Expression: "authorizer.requestResource.check('custom-verb').allowed()",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attributes: endpointStatusUpdateAttributes(),
|
||||||
|
results: []EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
|
||||||
|
ResourceRequest: true,
|
||||||
|
APIGroup: "",
|
||||||
|
Resource: "endpoints",
|
||||||
|
Subresource: "status",
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "endpoints1",
|
||||||
|
Verb: "custom-verb",
|
||||||
|
APIVersion: "*",
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "test serviceAccount authorizer allow check",
|
||||||
|
validations: []ExpressionAccessor{
|
||||||
|
&condition{
|
||||||
|
Expression: "authorizer.serviceAccount('default', 'test-serviceaccount').group('').resource('endpoints').namespace('default').name('endpoints1').check('custom-verb').allowed()",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
attributes: endpointCreateAttributes(),
|
||||||
|
results: []EvaluationResult{
|
||||||
|
{
|
||||||
|
EvalResult: celtypes.True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
authorizer: newAuthzAllowMatch(authorizer.AttributesRecord{
|
||||||
|
User: &user.DefaultInfo{
|
||||||
|
Name: "system:serviceaccount:default:test-serviceaccount",
|
||||||
|
Groups: []string{"system:serviceaccounts", "system:serviceaccounts:default"},
|
||||||
|
},
|
||||||
|
ResourceRequest: true,
|
||||||
|
APIGroup: "",
|
||||||
|
Resource: "endpoints",
|
||||||
|
Namespace: "default",
|
||||||
|
Name: "endpoints1",
|
||||||
|
Verb: "custom-verb",
|
||||||
|
APIVersion: "*",
|
||||||
|
}),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tc := range cases {
|
for _, tc := range cases {
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
c := filterCompiler{}
|
c := filterCompiler{}
|
||||||
f := c.Compile(tc.validations, tc.hasParamKind)
|
f := c.Compile(tc.validations, OptionalVariableDeclarations{HasParams: tc.hasParamKind, HasAuthorizer: tc.authorizer != nil})
|
||||||
if f == nil {
|
if f == nil {
|
||||||
t.Fatalf("unexpected nil validator")
|
t.Fatalf("unexpected nil validator")
|
||||||
}
|
}
|
||||||
@ -435,7 +634,8 @@ func TestFilter(t *testing.T) {
|
|||||||
t.Fatalf("unexpected error on conversion: %v", err)
|
t.Fatalf("unexpected error on conversion: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
evalResults, err := f.ForInput(versionedAttr, tc.params, CreateAdmissionRequest(versionedAttr.Attributes))
|
optionalVars := OptionalVariableBindings{VersionedParams: tc.params, Authorizer: tc.authorizer}
|
||||||
|
evalResults, err := f.ForInput(versionedAttr, CreateAdmissionRequest(versionedAttr.Attributes), optionalVars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("unexpected error: %v", err)
|
t.Fatalf("unexpected error: %v", err)
|
||||||
}
|
}
|
||||||
@ -582,3 +782,74 @@ func TestCompilationErrors(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var denyAll = fakeAuthorizer{defaultResult: authorizerResult{decision: authorizer.DecisionDeny, reason: "fake reason", err: nil}}
|
||||||
|
|
||||||
|
func newAuthzAllowMatch(match authorizer.AttributesRecord) fakeAuthorizer {
|
||||||
|
return fakeAuthorizer{
|
||||||
|
match: &authorizerMatch{
|
||||||
|
match: match,
|
||||||
|
authorizerResult: authorizerResult{decision: authorizer.DecisionAllow, reason: "", err: nil},
|
||||||
|
},
|
||||||
|
defaultResult: authorizerResult{decision: authorizer.DecisionDeny, reason: "", err: nil},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakeAuthorizer struct {
|
||||||
|
match *authorizerMatch
|
||||||
|
defaultResult authorizerResult
|
||||||
|
}
|
||||||
|
|
||||||
|
type authorizerResult struct {
|
||||||
|
decision authorizer.Decision
|
||||||
|
reason string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
type authorizerMatch struct {
|
||||||
|
authorizerResult
|
||||||
|
match authorizer.AttributesRecord
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||||
|
if f.match != nil {
|
||||||
|
other, ok := a.(*authorizer.AttributesRecord)
|
||||||
|
if !ok {
|
||||||
|
panic(fmt.Sprintf("unsupported type: %T", a))
|
||||||
|
}
|
||||||
|
if reflect.DeepEqual(f.match.match, *other) {
|
||||||
|
return f.match.decision, f.match.reason, f.match.err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return f.defaultResult.decision, f.defaultResult.reason, f.defaultResult.err
|
||||||
|
}
|
||||||
|
|
||||||
|
func endpointCreateAttributes() admission.Attributes {
|
||||||
|
name := "endpoints1"
|
||||||
|
namespace := "default"
|
||||||
|
var object, oldObject runtime.Object
|
||||||
|
object = &corev1.Endpoints{
|
||||||
|
TypeMeta: metav1.TypeMeta{
|
||||||
|
Kind: "Endpoints",
|
||||||
|
APIVersion: "v1",
|
||||||
|
},
|
||||||
|
ObjectMeta: metav1.ObjectMeta{
|
||||||
|
Name: name,
|
||||||
|
},
|
||||||
|
Subsets: []corev1.EndpointSubset{
|
||||||
|
{
|
||||||
|
Addresses: []corev1.EndpointAddress{{IP: "127.0.0.0"}},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
gvk := schema.GroupVersionKind{Group: "", Version: "v1", Kind: "Endpoints"}
|
||||||
|
gvr := schema.GroupVersionResource{Group: "", Version: "v1", Resource: "endpoints"}
|
||||||
|
return admission.NewAttributesRecord(object, oldObject, gvk, namespace, name, gvr, "", admission.Create, &metav1.CreateOptions{}, false, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func endpointStatusUpdateAttributes() admission.Attributes {
|
||||||
|
attrs := endpointCreateAttributes()
|
||||||
|
return admission.NewAttributesRecord(
|
||||||
|
attrs.GetObject(), attrs.GetObject(), attrs.GetKind(), attrs.GetNamespace(), attrs.GetName(),
|
||||||
|
attrs.GetResource(), "status", admission.Update, &metav1.UpdateOptions{}, false, nil)
|
||||||
|
}
|
||||||
|
@ -24,6 +24,7 @@ import (
|
|||||||
v1 "k8s.io/api/admission/v1"
|
v1 "k8s.io/api/admission/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
)
|
)
|
||||||
|
|
||||||
var _ ExpressionAccessor = &MatchCondition{}
|
var _ ExpressionAccessor = &MatchCondition{}
|
||||||
@ -49,19 +50,41 @@ func (v *MatchCondition) GetExpression() string {
|
|||||||
return v.Expression
|
return v.Expression
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OptionalVariableDeclarations declares which optional CEL variables
|
||||||
|
// are declared for an expression.
|
||||||
|
type OptionalVariableDeclarations struct {
|
||||||
|
// HasParams specifies if the "params" variable is declared.
|
||||||
|
// The "params" variable may still be bound to "null" when declared.
|
||||||
|
HasParams bool
|
||||||
|
// HasAuthorizer specifies if the"authorizer" and "authorizer.requestResource"
|
||||||
|
// variables are declared. When declared, the authorizer variables are
|
||||||
|
// expected to be non-null.
|
||||||
|
HasAuthorizer bool
|
||||||
|
}
|
||||||
|
|
||||||
// FilterCompiler contains a function to assist with converting types and values to/from CEL-typed values.
|
// FilterCompiler contains a function to assist with converting types and values to/from CEL-typed values.
|
||||||
type FilterCompiler interface {
|
type FilterCompiler interface {
|
||||||
// Compile is used for the cel expression compilation
|
// Compile is used for the cel expression compilation
|
||||||
Compile(expressions []ExpressionAccessor, hasParam bool) Filter
|
Compile(expressions []ExpressionAccessor, optionalDecls OptionalVariableDeclarations) Filter
|
||||||
|
}
|
||||||
|
|
||||||
|
// OptionalVariableBindings provides expression bindings for optional CEL variables.
|
||||||
|
type OptionalVariableBindings struct {
|
||||||
|
// VersionedParams provides the "params" variable binding. This variable binding may
|
||||||
|
// be set to nil even when OptionalVariableDeclarations.HashParams is set to true.
|
||||||
|
VersionedParams runtime.Object
|
||||||
|
// Authorizer provides the authorizer used for the "authorizer" and
|
||||||
|
// "authorizer.requestResource" variable bindings. If the expression was compiled with
|
||||||
|
// OptionalVariableDeclarations.HasAuthorizer set to true this must be non-nil.
|
||||||
|
Authorizer authorizer.Authorizer
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter contains a function to evaluate compiled CEL-typed values
|
// Filter contains a function to evaluate compiled CEL-typed values
|
||||||
// It expects the inbound object to already have been converted to the version expected
|
// It expects the inbound object to already have been converted to the version expected
|
||||||
// by the underlying CEL code (which is indicated by the match criteria of a policy definition).
|
// by the underlying CEL code (which is indicated by the match criteria of a policy definition).
|
||||||
// versionedParams may be nil.
|
|
||||||
type Filter interface {
|
type Filter interface {
|
||||||
// ForInput converts compiled CEL-typed values into evaluated CEL-typed values
|
// ForInput converts compiled CEL-typed values into evaluated CEL-typed values
|
||||||
ForInput(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, request *v1.AdmissionRequest) ([]EvaluationResult, error)
|
ForInput(versionedAttr *generic.VersionedAttributes, request *v1.AdmissionRequest, optionalVars OptionalVariableBindings) ([]EvaluationResult, error)
|
||||||
|
|
||||||
// CompilationErrors returns a list of errors from the compilation of the evaluator
|
// CompilationErrors returns a list of errors from the compilation of the evaluator
|
||||||
CompilationErrors() []error
|
CompilationErrors() []error
|
||||||
|
@ -23,6 +23,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
|
|
||||||
"k8s.io/apimachinery/pkg/api/meta"
|
"k8s.io/apimachinery/pkg/api/meta"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/features"
|
"k8s.io/apiserver/pkg/features"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/component-base/featuregate"
|
"k8s.io/component-base/featuregate"
|
||||||
@ -71,6 +72,7 @@ type celAdmissionPlugin struct {
|
|||||||
restMapper meta.RESTMapper
|
restMapper meta.RESTMapper
|
||||||
dynamicClient dynamic.Interface
|
dynamicClient dynamic.Interface
|
||||||
stopCh <-chan struct{}
|
stopCh <-chan struct{}
|
||||||
|
authorizer authorizer.Authorizer
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ initializer.WantsExternalKubeInformerFactory = &celAdmissionPlugin{}
|
var _ initializer.WantsExternalKubeInformerFactory = &celAdmissionPlugin{}
|
||||||
@ -78,6 +80,7 @@ var _ initializer.WantsExternalKubeClientSet = &celAdmissionPlugin{}
|
|||||||
var _ initializer.WantsRESTMapper = &celAdmissionPlugin{}
|
var _ initializer.WantsRESTMapper = &celAdmissionPlugin{}
|
||||||
var _ initializer.WantsDynamicClient = &celAdmissionPlugin{}
|
var _ initializer.WantsDynamicClient = &celAdmissionPlugin{}
|
||||||
var _ initializer.WantsDrainedNotification = &celAdmissionPlugin{}
|
var _ initializer.WantsDrainedNotification = &celAdmissionPlugin{}
|
||||||
|
var _ initializer.WantsAuthorizer = &celAdmissionPlugin{}
|
||||||
|
|
||||||
var _ admission.InitializationValidator = &celAdmissionPlugin{}
|
var _ admission.InitializationValidator = &celAdmissionPlugin{}
|
||||||
var _ admission.ValidationInterface = &celAdmissionPlugin{}
|
var _ admission.ValidationInterface = &celAdmissionPlugin{}
|
||||||
@ -108,6 +111,10 @@ func (c *celAdmissionPlugin) SetDrainedNotification(stopCh <-chan struct{}) {
|
|||||||
c.stopCh = stopCh
|
c.stopCh = stopCh
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *celAdmissionPlugin) SetAuthorizer(authorizer authorizer.Authorizer) {
|
||||||
|
c.authorizer = authorizer
|
||||||
|
}
|
||||||
|
|
||||||
func (c *celAdmissionPlugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
|
func (c *celAdmissionPlugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
|
||||||
if featureGates.Enabled(features.ValidatingAdmissionPolicy) {
|
if featureGates.Enabled(features.ValidatingAdmissionPolicy) {
|
||||||
c.enabled = true
|
c.enabled = true
|
||||||
@ -138,7 +145,10 @@ func (c *celAdmissionPlugin) ValidateInitialization() error {
|
|||||||
if c.stopCh == nil {
|
if c.stopCh == nil {
|
||||||
return errors.New("missing stop channel")
|
return errors.New("missing stop channel")
|
||||||
}
|
}
|
||||||
c.evaluator = NewAdmissionController(c.informerFactory, c.client, c.restMapper, c.dynamicClient)
|
if c.authorizer == nil {
|
||||||
|
return errors.New("missing authorizer")
|
||||||
|
}
|
||||||
|
c.evaluator = NewAdmissionController(c.informerFactory, c.client, c.restMapper, c.dynamicClient, c.authorizer)
|
||||||
if err := c.evaluator.ValidateInitialization(); err != nil {
|
if err := c.evaluator.ValidateInitialization(); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -26,6 +26,8 @@ import (
|
|||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
admissionv1 "k8s.io/api/admission/v1"
|
admissionv1 "k8s.io/api/admission/v1"
|
||||||
admissionRegistrationv1 "k8s.io/api/admissionregistration/v1"
|
admissionRegistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||||
"k8s.io/api/admissionregistration/v1alpha1"
|
"k8s.io/api/admissionregistration/v1alpha1"
|
||||||
@ -42,6 +44,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
|
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
|
||||||
whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/apiserver/pkg/features"
|
"k8s.io/apiserver/pkg/features"
|
||||||
dynamicfake "k8s.io/client-go/dynamic/fake"
|
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
@ -49,7 +52,6 @@ import (
|
|||||||
clienttesting "k8s.io/client-go/testing"
|
clienttesting "k8s.io/client-go/testing"
|
||||||
"k8s.io/client-go/tools/cache"
|
"k8s.io/client-go/tools/cache"
|
||||||
"k8s.io/component-base/featuregate"
|
"k8s.io/component-base/featuregate"
|
||||||
"k8s.io/klog/v2"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -158,7 +160,7 @@ var (
|
|||||||
// Interface which has fake compile functionality for use in tests
|
// Interface which has fake compile functionality for use in tests
|
||||||
// So that we can test the controller without pulling in any CEL functionality
|
// So that we can test the controller without pulling in any CEL functionality
|
||||||
type fakeCompiler struct {
|
type fakeCompiler struct {
|
||||||
CompileFuncs map[string]func([]cel.ExpressionAccessor, bool) cel.Filter
|
CompileFuncs map[string]func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ cel.FilterCompiler = &fakeCompiler{}
|
var _ cel.FilterCompiler = &fakeCompiler{}
|
||||||
@ -169,22 +171,22 @@ func (f *fakeCompiler) HasSynced() bool {
|
|||||||
|
|
||||||
func (f *fakeCompiler) Compile(
|
func (f *fakeCompiler) Compile(
|
||||||
expressions []cel.ExpressionAccessor,
|
expressions []cel.ExpressionAccessor,
|
||||||
hasParam bool,
|
options cel.OptionalVariableDeclarations,
|
||||||
) cel.Filter {
|
) cel.Filter {
|
||||||
key := expressions[0].GetExpression()
|
key := expressions[0].GetExpression()
|
||||||
if fun, ok := f.CompileFuncs[key]; ok {
|
if fun, ok := f.CompileFuncs[key]; ok {
|
||||||
return fun(expressions, hasParam)
|
return fun(expressions, options)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeCompiler) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, compileFunc func([]cel.ExpressionAccessor, bool) cel.Filter) {
|
func (f *fakeCompiler) RegisterDefinition(definition *v1alpha1.ValidatingAdmissionPolicy, compileFunc func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter) {
|
||||||
//Key must be something that we can decipher from the inputs to Validate so using expression which will be passed to validate on the filter
|
//Key must be something that we can decipher from the inputs to Validate so using expression which will be passed to validate on the filter
|
||||||
key := definition.Spec.Validations[0].Expression
|
key := definition.Spec.Validations[0].Expression
|
||||||
if compileFunc != nil {
|
if compileFunc != nil {
|
||||||
if f.CompileFuncs == nil {
|
if f.CompileFuncs == nil {
|
||||||
f.CompileFuncs = make(map[string]func([]cel.ExpressionAccessor, bool) cel.Filter)
|
f.CompileFuncs = make(map[string]func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter)
|
||||||
}
|
}
|
||||||
f.CompileFuncs[key] = compileFunc
|
f.CompileFuncs[key] = compileFunc
|
||||||
}
|
}
|
||||||
@ -206,7 +208,7 @@ type fakeFilter struct {
|
|||||||
keyId string
|
keyId string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeFilter) ForInput(versionedAttr *whgeneric.VersionedAttributes, versionedParams runtime.Object, request *admissionv1.AdmissionRequest) ([]cel.EvaluationResult, error) {
|
func (f *fakeFilter) ForInput(versionedAttr *whgeneric.VersionedAttributes, request *admissionv1.AdmissionRequest, inputs cel.OptionalVariableBindings) ([]cel.EvaluationResult, error) {
|
||||||
return []cel.EvaluationResult{}, nil
|
return []cel.EvaluationResult{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -333,6 +335,7 @@ func setupTestCommon(t *testing.T, compiler cel.FilterCompiler, matcher Matcher,
|
|||||||
testContext, testContextCancel := context.WithCancel(context.Background())
|
testContext, testContextCancel := context.WithCancel(context.Background())
|
||||||
t.Cleanup(testContextCancel)
|
t.Cleanup(testContextCancel)
|
||||||
|
|
||||||
|
fakeAuthorizer := fakeAuthorizer{}
|
||||||
dynamicClient := dynamicfake.NewSimpleDynamicClient(scheme)
|
dynamicClient := dynamicfake.NewSimpleDynamicClient(scheme)
|
||||||
|
|
||||||
fakeClient := fake.NewSimpleClientset()
|
fakeClient := fake.NewSimpleClientset()
|
||||||
@ -355,7 +358,7 @@ func setupTestCommon(t *testing.T, compiler cel.FilterCompiler, matcher Matcher,
|
|||||||
handler := plug.(*celAdmissionPlugin)
|
handler := plug.(*celAdmissionPlugin)
|
||||||
handler.enabled = true
|
handler.enabled = true
|
||||||
|
|
||||||
genericInitializer := initializer.New(fakeClient, dynamicClient, fakeInformerFactory, nil, featureGate, testContext.Done())
|
genericInitializer := initializer.New(fakeClient, dynamicClient, fakeInformerFactory, fakeAuthorizer, featureGate, testContext.Done())
|
||||||
genericInitializer.Initialize(handler)
|
genericInitializer.Initialize(handler)
|
||||||
handler.SetRESTMapper(fakeRestMapper)
|
handler.SetRESTMapper(fakeRestMapper)
|
||||||
err = admission.ValidateInitialization(handler)
|
err = admission.ValidateInitialization(handler)
|
||||||
@ -365,7 +368,7 @@ func setupTestCommon(t *testing.T, compiler cel.FilterCompiler, matcher Matcher,
|
|||||||
// Override compiler used by controller for tests
|
// Override compiler used by controller for tests
|
||||||
controller = handler.evaluator.(*celAdmissionController)
|
controller = handler.evaluator.(*celAdmissionController)
|
||||||
controller.policyController.filterCompiler = compiler
|
controller.policyController.filterCompiler = compiler
|
||||||
controller.policyController.newValidator = func(filter cel.Filter, fail *admissionRegistrationv1.FailurePolicyType) Validator {
|
controller.policyController.newValidator = func(filter cel.Filter, fail *admissionRegistrationv1.FailurePolicyType, authorizer authorizer.Authorizer) Validator {
|
||||||
f := filter.(*fakeFilter)
|
f := filter.(*fakeFilter)
|
||||||
v := validatorMap[f.keyId]
|
v := validatorMap[f.keyId]
|
||||||
v.fakeFilter = f
|
v.fakeFilter = f
|
||||||
@ -702,7 +705,7 @@ func TestBasicPolicyDefinitionFailure(t *testing.T) {
|
|||||||
DefaultMatch: true,
|
DefaultMatch: true,
|
||||||
}
|
}
|
||||||
|
|
||||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||||
datalock.Lock()
|
datalock.Lock()
|
||||||
numCompiles += 1
|
numCompiles += 1
|
||||||
datalock.Unlock()
|
datalock.Unlock()
|
||||||
@ -762,7 +765,7 @@ func TestDefinitionDoesntMatch(t *testing.T) {
|
|||||||
passedParams := []*unstructured.Unstructured{}
|
passedParams := []*unstructured.Unstructured{}
|
||||||
numCompiles := 0
|
numCompiles := 0
|
||||||
|
|
||||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||||
datalock.Lock()
|
datalock.Lock()
|
||||||
numCompiles += 1
|
numCompiles += 1
|
||||||
datalock.Unlock()
|
datalock.Unlock()
|
||||||
@ -873,7 +876,7 @@ func TestReconfigureBinding(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||||
datalock.Lock()
|
datalock.Lock()
|
||||||
numCompiles += 1
|
numCompiles += 1
|
||||||
datalock.Unlock()
|
datalock.Unlock()
|
||||||
@ -980,7 +983,7 @@ func TestRemoveDefinition(t *testing.T) {
|
|||||||
datalock := sync.Mutex{}
|
datalock := sync.Mutex{}
|
||||||
numCompiles := 0
|
numCompiles := 0
|
||||||
|
|
||||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||||
datalock.Lock()
|
datalock.Lock()
|
||||||
numCompiles += 1
|
numCompiles += 1
|
||||||
datalock.Unlock()
|
datalock.Unlock()
|
||||||
@ -1047,7 +1050,7 @@ func TestRemoveBinding(t *testing.T) {
|
|||||||
datalock := sync.Mutex{}
|
datalock := sync.Mutex{}
|
||||||
numCompiles := 0
|
numCompiles := 0
|
||||||
|
|
||||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||||
datalock.Lock()
|
datalock.Lock()
|
||||||
numCompiles += 1
|
numCompiles += 1
|
||||||
datalock.Unlock()
|
datalock.Unlock()
|
||||||
@ -1155,7 +1158,7 @@ func TestInvalidParamSourceInstanceName(t *testing.T) {
|
|||||||
passedParams := []*unstructured.Unstructured{}
|
passedParams := []*unstructured.Unstructured{}
|
||||||
numCompiles := 0
|
numCompiles := 0
|
||||||
|
|
||||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||||
datalock.Lock()
|
datalock.Lock()
|
||||||
numCompiles += 1
|
numCompiles += 1
|
||||||
datalock.Unlock()
|
datalock.Unlock()
|
||||||
@ -1221,7 +1224,7 @@ func TestEmptyParamSource(t *testing.T) {
|
|||||||
noParamSourcePolicy := *denyPolicy
|
noParamSourcePolicy := *denyPolicy
|
||||||
noParamSourcePolicy.Spec.ParamKind = nil
|
noParamSourcePolicy.Spec.ParamKind = nil
|
||||||
|
|
||||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||||
datalock.Lock()
|
datalock.Lock()
|
||||||
numCompiles += 1
|
numCompiles += 1
|
||||||
datalock.Unlock()
|
datalock.Unlock()
|
||||||
@ -1323,7 +1326,7 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) {
|
|||||||
compiles2 := atomic.Int64{}
|
compiles2 := atomic.Int64{}
|
||||||
evaluations2 := atomic.Int64{}
|
evaluations2 := atomic.Int64{}
|
||||||
|
|
||||||
compiler.RegisterDefinition(&policy1, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
compiler.RegisterDefinition(&policy1, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||||
compiles1.Add(1)
|
compiles1.Add(1)
|
||||||
|
|
||||||
return &fakeFilter{
|
return &fakeFilter{
|
||||||
@ -1340,7 +1343,7 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
compiler.RegisterDefinition(&policy2, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
compiler.RegisterDefinition(&policy2, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||||
compiles2.Add(1)
|
compiles2.Add(1)
|
||||||
|
|
||||||
return &fakeFilter{
|
return &fakeFilter{
|
||||||
@ -1448,7 +1451,7 @@ func TestNativeTypeParam(t *testing.T) {
|
|||||||
Kind: "ConfigMap",
|
Kind: "ConfigMap",
|
||||||
}
|
}
|
||||||
|
|
||||||
compiler.RegisterDefinition(&nativeTypeParamPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
compiler.RegisterDefinition(&nativeTypeParamPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||||
compiles.Add(1)
|
compiles.Add(1)
|
||||||
|
|
||||||
return &fakeFilter{
|
return &fakeFilter{
|
||||||
@ -1511,3 +1514,9 @@ func TestNativeTypeParam(t *testing.T) {
|
|||||||
require.EqualValues(t, 1, compiles.Load())
|
require.EqualValues(t, 1, compiles.Load())
|
||||||
require.EqualValues(t, 1, evaluations.Load())
|
require.EqualValues(t, 1, evaluations.Load())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fakeAuthorizer struct{}
|
||||||
|
|
||||||
|
func (f fakeAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||||
|
return authorizer.DecisionAllow, "", nil
|
||||||
|
}
|
||||||
|
@ -38,6 +38,7 @@ import (
|
|||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
|
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching"
|
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/matching"
|
||||||
whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
"k8s.io/client-go/kubernetes"
|
"k8s.io/client-go/kubernetes"
|
||||||
@ -119,6 +120,7 @@ func NewAdmissionController(
|
|||||||
client kubernetes.Interface,
|
client kubernetes.Interface,
|
||||||
restMapper meta.RESTMapper,
|
restMapper meta.RESTMapper,
|
||||||
dynamicClient dynamic.Interface,
|
dynamicClient dynamic.Interface,
|
||||||
|
authz authorizer.Authorizer,
|
||||||
) CELPolicyEvaluator {
|
) CELPolicyEvaluator {
|
||||||
return &celAdmissionController{
|
return &celAdmissionController{
|
||||||
definitions: atomic.Value{},
|
definitions: atomic.Value{},
|
||||||
@ -132,6 +134,7 @@ func NewAdmissionController(
|
|||||||
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies().Informer()),
|
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies().Informer()),
|
||||||
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicyBinding](
|
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicyBinding](
|
||||||
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicyBindings().Informer()),
|
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicyBindings().Informer()),
|
||||||
|
authz,
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -32,6 +32,7 @@ import (
|
|||||||
celmetrics "k8s.io/apiserver/pkg/admission/cel"
|
celmetrics "k8s.io/apiserver/pkg/admission/cel"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
|
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
"k8s.io/client-go/dynamic"
|
"k8s.io/client-go/dynamic"
|
||||||
"k8s.io/client-go/dynamic/dynamicinformer"
|
"k8s.io/client-go/dynamic/dynamicinformer"
|
||||||
"k8s.io/client-go/informers"
|
"k8s.io/client-go/informers"
|
||||||
@ -85,9 +86,11 @@ type policyController struct {
|
|||||||
definitionsToBindings map[namespacedName]sets.Set[namespacedName]
|
definitionsToBindings map[namespacedName]sets.Set[namespacedName]
|
||||||
|
|
||||||
client kubernetes.Interface
|
client kubernetes.Interface
|
||||||
|
|
||||||
|
authz authorizer.Authorizer
|
||||||
}
|
}
|
||||||
|
|
||||||
type newValidator func(cel.Filter, *v1.FailurePolicyType) Validator
|
type newValidator func(cel.Filter, *v1.FailurePolicyType, authorizer.Authorizer) Validator
|
||||||
|
|
||||||
func newPolicyController(
|
func newPolicyController(
|
||||||
restMapper meta.RESTMapper,
|
restMapper meta.RESTMapper,
|
||||||
@ -97,6 +100,7 @@ func newPolicyController(
|
|||||||
matcher Matcher,
|
matcher Matcher,
|
||||||
policiesInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicy],
|
policiesInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicy],
|
||||||
bindingsInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicyBinding],
|
bindingsInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicyBinding],
|
||||||
|
authz authorizer.Authorizer,
|
||||||
) *policyController {
|
) *policyController {
|
||||||
res := &policyController{}
|
res := &policyController{}
|
||||||
*res = policyController{
|
*res = policyController{
|
||||||
@ -126,6 +130,7 @@ func newPolicyController(
|
|||||||
restMapper: restMapper,
|
restMapper: restMapper,
|
||||||
dynamicClient: dynamicClient,
|
dynamicClient: dynamicClient,
|
||||||
client: client,
|
client: client,
|
||||||
|
authz: authz,
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@ -439,9 +444,11 @@ func (c *policyController) latestPolicyData() []policyData {
|
|||||||
if definitionInfo.lastReconciledValue.Spec.ParamKind != nil {
|
if definitionInfo.lastReconciledValue.Spec.ParamKind != nil {
|
||||||
hasParam = true
|
hasParam = true
|
||||||
}
|
}
|
||||||
|
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
|
||||||
bindingInfo.validator = c.newValidator(
|
bindingInfo.validator = c.newValidator(
|
||||||
c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), hasParam),
|
c.filterCompiler.Compile(convertv1alpha1Validations(definitionInfo.lastReconciledValue.Spec.Validations), optionalVars),
|
||||||
convertv1alpha1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy),
|
convertv1alpha1FailurePolicyTypeTov1FailurePolicyType(definitionInfo.lastReconciledValue.Spec.FailurePolicy),
|
||||||
|
c.authz,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
bindingInfos = append(bindingInfos, *bindingInfo)
|
bindingInfos = append(bindingInfos, *bindingInfo)
|
||||||
|
@ -18,6 +18,7 @@ package validatingadmissionpolicy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -18,28 +18,31 @@ package validatingadmissionpolicy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"k8s.io/klog/v2"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
celtypes "github.com/google/cel-go/common/types"
|
celtypes "github.com/google/cel-go/common/types"
|
||||||
|
"k8s.io/klog/v2"
|
||||||
|
|
||||||
v1 "k8s.io/api/admissionregistration/v1"
|
v1 "k8s.io/api/admissionregistration/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
"k8s.io/apimachinery/pkg/runtime"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
)
|
)
|
||||||
|
|
||||||
// validator implements the Validator interface
|
// validator implements the Validator interface
|
||||||
type validator struct {
|
type validator struct {
|
||||||
filter cel.Filter
|
filter cel.Filter
|
||||||
failPolicy *v1.FailurePolicyType
|
failPolicy *v1.FailurePolicyType
|
||||||
|
authorizer authorizer.Authorizer
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewValidator(filter cel.Filter, failPolicy *v1.FailurePolicyType) Validator {
|
func NewValidator(filter cel.Filter, failPolicy *v1.FailurePolicyType, authorizer authorizer.Authorizer) Validator {
|
||||||
return &validator{
|
return &validator{
|
||||||
filter: filter,
|
filter: filter,
|
||||||
failPolicy: failPolicy,
|
failPolicy: failPolicy,
|
||||||
|
authorizer: authorizer,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -59,7 +62,8 @@ func (v *validator) Validate(versionedAttr *generic.VersionedAttributes, version
|
|||||||
f = *v.failPolicy
|
f = *v.failPolicy
|
||||||
}
|
}
|
||||||
|
|
||||||
evalResults, err := v.filter.ForInput(versionedAttr, versionedParams, cel.CreateAdmissionRequest(versionedAttr.Attributes))
|
optionalVars := cel.OptionalVariableBindings{VersionedParams: versionedParams, Authorizer: v.authorizer}
|
||||||
|
evalResults, err := v.filter.ForInput(versionedAttr, cel.CreateAdmissionRequest(versionedAttr.Attributes), optionalVars)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return []PolicyDecision{
|
return []PolicyDecision{
|
||||||
{
|
{
|
||||||
|
@ -28,7 +28,6 @@ import (
|
|||||||
admissionv1 "k8s.io/api/admission/v1"
|
admissionv1 "k8s.io/api/admission/v1"
|
||||||
v1 "k8s.io/api/admissionregistration/v1"
|
v1 "k8s.io/api/admissionregistration/v1"
|
||||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||||
"k8s.io/apimachinery/pkg/runtime"
|
|
||||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
"k8s.io/apiserver/pkg/admission"
|
"k8s.io/apiserver/pkg/admission"
|
||||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||||
@ -42,7 +41,7 @@ type fakeCelFilter struct {
|
|||||||
throwError bool
|
throwError bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func (f *fakeCelFilter) ForInput(versionedAttr *generic.VersionedAttributes, versionedParams runtime.Object, request *admissionv1.AdmissionRequest) ([]cel.EvaluationResult, error) {
|
func (f *fakeCelFilter) ForInput(*generic.VersionedAttributes, *admissionv1.AdmissionRequest, cel.OptionalVariableBindings) ([]cel.EvaluationResult, error) {
|
||||||
if f.throwError {
|
if f.throwError {
|
||||||
return nil, errors.New("test error")
|
return nil, errors.New("test error")
|
||||||
}
|
}
|
||||||
@ -465,6 +464,7 @@ func TestValidate(t *testing.T) {
|
|||||||
throwError: tc.throwError,
|
throwError: tc.throwError,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
policyResults := v.Validate(fakeVersionedAttr, nil)
|
policyResults := v.Validate(fakeVersionedAttr, nil)
|
||||||
|
|
||||||
require.Equal(t, len(policyResults), len(tc.policyDecision))
|
require.Equal(t, len(policyResults), len(tc.policyDecision))
|
||||||
|
580
staging/src/k8s.io/apiserver/pkg/cel/library/authz.go
Normal file
580
staging/src/k8s.io/apiserver/pkg/cel/library/authz.go
Normal file
@ -0,0 +1,580 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2023 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 library
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/google/cel-go/cel"
|
||||||
|
"github.com/google/cel-go/common/types"
|
||||||
|
"github.com/google/cel-go/common/types/ref"
|
||||||
|
|
||||||
|
apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation"
|
||||||
|
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/serviceaccount"
|
||||||
|
"k8s.io/apiserver/pkg/authentication/user"
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Authz provides a CEL function library extension for performing authorization checks.
|
||||||
|
// Note that authorization checks are only supported for CEL expression fields in the API
|
||||||
|
// where an 'authorizer' variable is provided to the CEL expression. See the
|
||||||
|
// documentation of API fields where CEL expressions are used to learn if the 'authorizer'
|
||||||
|
// variable is provided.
|
||||||
|
//
|
||||||
|
// path
|
||||||
|
//
|
||||||
|
// Returns a PathCheck configured to check authorization for a non-resource request
|
||||||
|
// path (e.g. /healthz). If path is an empty string, an error is returned.
|
||||||
|
// Note that the leading '/' is not required.
|
||||||
|
//
|
||||||
|
// <Authorizer>.path(<string>) <PathCheck>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// authorizer.path('/healthz') // returns a PathCheck for the '/healthz' API path
|
||||||
|
// authorizer.path('') // results in "path must not be empty" error
|
||||||
|
// authorizer.path(' ') // results in "path must not be empty" error
|
||||||
|
//
|
||||||
|
// group
|
||||||
|
//
|
||||||
|
// Returns a GroupCheck configured to check authorization for the API resources for
|
||||||
|
// a particular API group.
|
||||||
|
// Note that authorization checks are only supported for CEL expression fields in the API
|
||||||
|
// where an 'authorizer' variable is provided to the CEL expression. Check the
|
||||||
|
// documentation of API fields where CEL expressions are used to learn if the 'authorizer'
|
||||||
|
// variable is provided.
|
||||||
|
//
|
||||||
|
// <Authorizer>.group(<string>) <GroupCheck>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// authorizer.group('apps') // returns a GroupCheck for the 'apps' API group
|
||||||
|
// authorizer.group('') // returns a GroupCheck for the core API group
|
||||||
|
// authorizer.group('example.com') // returns a GroupCheck for the custom resources in the 'example.com' API group
|
||||||
|
//
|
||||||
|
// serviceAccount
|
||||||
|
//
|
||||||
|
// Returns an Authorizer configured to check authorization for the provided service account namespace and name.
|
||||||
|
// If the name is not a valid DNS subdomain string (as defined by RFC 1123), an error is returned.
|
||||||
|
// If the namespace is not a valid DNS label (as defined by RFC 1123), an error is returned.
|
||||||
|
//
|
||||||
|
// <Authorizer>.serviceAccount(<string>, <string>) <Authorizer>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// authorizer.serviceAccount('default', 'myserviceaccount') // returns an Authorizer for the service account with namespace 'default' and name 'myserviceaccount'
|
||||||
|
// authorizer.serviceAccount('not@a#valid!namespace', 'validname') // returns an error
|
||||||
|
// authorizer.serviceAccount('valid.example.com', 'invalid@*name') // returns an error
|
||||||
|
//
|
||||||
|
// resource
|
||||||
|
//
|
||||||
|
// Returns a ResourceCheck configured to check authorization for a particular API resource.
|
||||||
|
// Note that the provided resource string should be a lower case plural name of a Kubernetes API resource.
|
||||||
|
//
|
||||||
|
// <GroupCheck>.resource(<string>) <ResourceCheck>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// authorizer.group('apps').resource('deployments') // returns a ResourceCheck for the 'deployments' resources in the 'apps' group.
|
||||||
|
// authorizer.group('').resource('pods') // returns a ResourceCheck for the 'pods' resources in the core group.
|
||||||
|
// authorizer.group('apps').resource('') // results in "resource must not be empty" error
|
||||||
|
// authorizer.group('apps').resource(' ') // results in "resource must not be empty" error
|
||||||
|
//
|
||||||
|
// subresource
|
||||||
|
//
|
||||||
|
// Returns a ResourceCheck configured to check authorization for a particular subresource of an API resource.
|
||||||
|
// If subresource is set to "", the subresource field of this ResourceCheck is considered unset.
|
||||||
|
//
|
||||||
|
// <ResourceCheck>.subresource(<string>) <ResourceCheck>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// authorizer.group('').resource('pods').subresource('status') // returns a ResourceCheck the 'status' subresource of 'pods'
|
||||||
|
// authorizer.group('apps').resource('deployments').subresource('scale') // returns a ResourceCheck the 'scale' subresource of 'deployments'
|
||||||
|
// authorizer.group('example.com').resource('widgets').subresource('scale') // returns a ResourceCheck for the 'scale' subresource of the 'widgets' custom resource
|
||||||
|
// authorizer.group('example.com').resource('widgets').subresource('') // returns a ResourceCheck for the 'widgets' resource.
|
||||||
|
//
|
||||||
|
// namespace
|
||||||
|
//
|
||||||
|
// Returns a ResourceCheck configured to check authorization for a particular namespace.
|
||||||
|
// For cluster scoped resources, namespace() does not need to be called; namespace defaults
|
||||||
|
// to "", which is the correct namespace value to use to check cluster scoped resources.
|
||||||
|
// If namespace is set to "", the ResourceCheck will check authorization for the cluster scope.
|
||||||
|
//
|
||||||
|
// <ResourceCheck>.namespace(<string>) <ResourceCheck>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// authorizer.group('apps').resource('deployments').namespace('test') // returns a ResourceCheck for 'deployments' in the 'test' namespace
|
||||||
|
// authorizer.group('').resource('pods').namespace('default') // returns a ResourceCheck for 'pods' in the 'default' namespace
|
||||||
|
// authorizer.group('').resource('widgets').namespace('') // returns a ResourceCheck for 'widgets' in the cluster scope
|
||||||
|
//
|
||||||
|
// name
|
||||||
|
//
|
||||||
|
// Returns a ResourceCheck configured to check authorization for a particular resource name.
|
||||||
|
// If name is set to "", the name field of this ResourceCheck is considered unset.
|
||||||
|
//
|
||||||
|
// <ResourceCheck>.name(<name>) <ResourceCheck>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// authorizer.group('apps').resource('deployments').namespace('test').name('backend') // returns a ResourceCheck for the 'backend' 'deployments' resource in the 'test' namespace
|
||||||
|
// authorizer.group('apps').resource('deployments').namespace('test').name('') // returns a ResourceCheck for the 'deployments' resource in the 'test' namespace
|
||||||
|
//
|
||||||
|
// check
|
||||||
|
//
|
||||||
|
// For PathCheck, checks if the principal (user or service account) that sent the request is authorized for the HTTP request verb of the path.
|
||||||
|
// For ResourceCheck, checks if the principal (user or service account) that sent the request is authorized for the API verb and the configured authorization checks of the ResourceCheck.
|
||||||
|
// The check operation can be expensive, particularly in clusters using the webhook authorization mode.
|
||||||
|
//
|
||||||
|
// <PathCheck>.check(<check>) <Decision>
|
||||||
|
// <ResourceCheck>.check(<check>) <Decision>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// authorizer.group('').resource('pods').namespace('default').check('create') // Checks if the principal (user or service account) is authorized create pods in the 'default' namespace.
|
||||||
|
// authorizer.path('/healthz').check('get') // Checks if the principal (user or service account) is authorized to make HTTP GET requests to the /healthz API path.
|
||||||
|
//
|
||||||
|
// allowed
|
||||||
|
//
|
||||||
|
// Returns true if the authorizer's decision for the check is "allow". Note that if the authorizer's decision is
|
||||||
|
// "no opinion", that both the 'allowed' and 'denied' functions will return false.
|
||||||
|
//
|
||||||
|
// <Decision>.allowed() <bool>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// authorizer.group('').resource('pods').namespace('default').check('create').allowed() // Returns true if the principal (user or service account) is allowed create pods in the 'default' namespace.
|
||||||
|
// authorizer.path('/healthz').check('get').allowed() // Returns true if the principal (user or service account) is allowed to make HTTP GET requests to the /healthz API path.
|
||||||
|
//
|
||||||
|
// reason
|
||||||
|
//
|
||||||
|
// Returns a string reason for the authorization decision
|
||||||
|
//
|
||||||
|
// <Decision>.reason() <string>
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
//
|
||||||
|
// authorizer.path('/healthz').check('GET').reason()
|
||||||
|
func Authz() cel.EnvOption {
|
||||||
|
return cel.Lib(authzLib)
|
||||||
|
}
|
||||||
|
|
||||||
|
var authzLib = &authz{}
|
||||||
|
|
||||||
|
type authz struct{}
|
||||||
|
|
||||||
|
var authzLibraryDecls = map[string][]cel.FunctionOpt{
|
||||||
|
"path": {
|
||||||
|
cel.MemberOverload("authorizer_path", []*cel.Type{AuthorizerType, cel.StringType}, PathCheckType,
|
||||||
|
cel.BinaryBinding(authorizerPath))},
|
||||||
|
"group": {
|
||||||
|
cel.MemberOverload("authorizer_group", []*cel.Type{AuthorizerType, cel.StringType}, GroupCheckType,
|
||||||
|
cel.BinaryBinding(authorizerGroup))},
|
||||||
|
"serviceAccount": {
|
||||||
|
cel.MemberOverload("authorizer_serviceaccount", []*cel.Type{AuthorizerType, cel.StringType, cel.StringType}, AuthorizerType,
|
||||||
|
cel.FunctionBinding(authorizerServiceAccount))},
|
||||||
|
"resource": {
|
||||||
|
cel.MemberOverload("groupcheck_resource", []*cel.Type{GroupCheckType, cel.StringType}, ResourceCheckType,
|
||||||
|
cel.BinaryBinding(groupCheckResource))},
|
||||||
|
"subresource": {
|
||||||
|
cel.MemberOverload("resourcecheck_subresource", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
||||||
|
cel.BinaryBinding(resourceCheckSubresource))},
|
||||||
|
"namespace": {
|
||||||
|
cel.MemberOverload("resourcecheck_namespace", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
||||||
|
cel.BinaryBinding(resourceCheckNamespace))},
|
||||||
|
"name": {
|
||||||
|
cel.MemberOverload("resourcecheck_name", []*cel.Type{ResourceCheckType, cel.StringType}, ResourceCheckType,
|
||||||
|
cel.BinaryBinding(resourceCheckName))},
|
||||||
|
"check": {
|
||||||
|
cel.MemberOverload("pathcheck_check", []*cel.Type{PathCheckType, cel.StringType}, DecisionType,
|
||||||
|
cel.BinaryBinding(pathCheckCheck)),
|
||||||
|
cel.MemberOverload("resourcecheck_check", []*cel.Type{ResourceCheckType, cel.StringType}, DecisionType,
|
||||||
|
cel.BinaryBinding(resourceCheckCheck))},
|
||||||
|
"allowed": {
|
||||||
|
cel.MemberOverload("decision_allowed", []*cel.Type{DecisionType}, cel.BoolType,
|
||||||
|
cel.UnaryBinding(decisionAllowed))},
|
||||||
|
"reason": {
|
||||||
|
cel.MemberOverload("decision_reason", []*cel.Type{DecisionType}, cel.StringType,
|
||||||
|
cel.UnaryBinding(decisionReason))},
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*authz) CompileOptions() []cel.EnvOption {
|
||||||
|
options := make([]cel.EnvOption, 0, len(authzLibraryDecls))
|
||||||
|
for name, overloads := range authzLibraryDecls {
|
||||||
|
options = append(options, cel.Function(name, overloads...))
|
||||||
|
}
|
||||||
|
return options
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*authz) ProgramOptions() []cel.ProgramOption {
|
||||||
|
return []cel.ProgramOption{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func authorizerPath(arg1, arg2 ref.Val) ref.Val {
|
||||||
|
authz, ok := arg1.(authorizerVal)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
path, ok := arg2.Value().(string)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(strings.TrimSpace(path)) == 0 {
|
||||||
|
return types.NewErr("path must not be empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
return authz.pathCheck(path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func authorizerGroup(arg1, arg2 ref.Val) ref.Val {
|
||||||
|
authz, ok := arg1.(authorizerVal)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
group, ok := arg2.Value().(string)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return authz.groupCheck(group)
|
||||||
|
}
|
||||||
|
|
||||||
|
func authorizerServiceAccount(args ...ref.Val) ref.Val {
|
||||||
|
argn := len(args)
|
||||||
|
if argn != 3 {
|
||||||
|
return types.NoSuchOverloadErr()
|
||||||
|
}
|
||||||
|
|
||||||
|
authz, ok := args[0].(authorizerVal)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(args[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace, ok := args[1].Value().(string)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(args[1])
|
||||||
|
}
|
||||||
|
|
||||||
|
name, ok := args[2].Value().(string)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(args[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
if errors := apimachineryvalidation.ValidateServiceAccountName(name, false); len(errors) > 0 {
|
||||||
|
return types.NewErr("Invalid service account name")
|
||||||
|
}
|
||||||
|
if errors := apimachineryvalidation.ValidateNamespaceName(namespace, false); len(errors) > 0 {
|
||||||
|
return types.NewErr("Invalid service account namespace")
|
||||||
|
}
|
||||||
|
return authz.serviceAccount(namespace, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func groupCheckResource(arg1, arg2 ref.Val) ref.Val {
|
||||||
|
groupCheck, ok := arg1.(groupCheckVal)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
resource, ok := arg2.Value().(string)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(strings.TrimSpace(resource)) == 0 {
|
||||||
|
return types.NewErr("resource must not be empty")
|
||||||
|
}
|
||||||
|
return groupCheck.resourceCheck(resource)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCheckSubresource(arg1, arg2 ref.Val) ref.Val {
|
||||||
|
resourceCheck, ok := arg1.(resourceCheckVal)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
subresource, ok := arg2.Value().(string)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := resourceCheck
|
||||||
|
result.subresource = subresource
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCheckNamespace(arg1, arg2 ref.Val) ref.Val {
|
||||||
|
resourceCheck, ok := arg1.(resourceCheckVal)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
namespace, ok := arg2.Value().(string)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := resourceCheck
|
||||||
|
result.namespace = namespace
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCheckName(arg1, arg2 ref.Val) ref.Val {
|
||||||
|
resourceCheck, ok := arg1.(resourceCheckVal)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
name, ok := arg2.Value().(string)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
result := resourceCheck
|
||||||
|
result.name = name
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func pathCheckCheck(arg1, arg2 ref.Val) ref.Val {
|
||||||
|
pathCheck, ok := arg1.(pathCheckVal)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
httpRequestVerb, ok := arg2.Value().(string)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathCheck.Authorize(context.TODO(), httpRequestVerb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func resourceCheckCheck(arg1, arg2 ref.Val) ref.Val {
|
||||||
|
resourceCheck, ok := arg1.(resourceCheckVal)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
apiVerb, ok := arg2.Value().(string)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg1)
|
||||||
|
}
|
||||||
|
|
||||||
|
return resourceCheck.Authorize(context.TODO(), apiVerb)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decisionAllowed(arg ref.Val) ref.Val {
|
||||||
|
decision, ok := arg.(decisionVal)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.Bool(decision.authDecision == authorizer.DecisionAllow)
|
||||||
|
}
|
||||||
|
|
||||||
|
func decisionReason(arg ref.Val) ref.Val {
|
||||||
|
decision, ok := arg.(decisionVal)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(arg)
|
||||||
|
}
|
||||||
|
|
||||||
|
return types.String(decision.reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
AuthorizerType = cel.ObjectType("kubernetes.authorization.Authorizer")
|
||||||
|
PathCheckType = cel.ObjectType("kubernetes.authorization.PathCheck")
|
||||||
|
GroupCheckType = cel.ObjectType("kubernetes.authorization.GroupCheck")
|
||||||
|
ResourceCheckType = cel.ObjectType("kubernetes.authorization.ResourceCheck")
|
||||||
|
DecisionType = cel.ObjectType("kubernetes.authorization.Decision")
|
||||||
|
)
|
||||||
|
|
||||||
|
// Resource represents an API resource
|
||||||
|
type Resource interface {
|
||||||
|
// GetName returns the name of the object as presented in the request. On a CREATE operation, the client
|
||||||
|
// may omit name and rely on the server to generate the name. If that is the case, this method will return
|
||||||
|
// the empty string
|
||||||
|
GetName() string
|
||||||
|
// GetNamespace is the namespace associated with the request (if any)
|
||||||
|
GetNamespace() string
|
||||||
|
// GetResource is the name of the resource being requested. This is not the kind. For example: pods
|
||||||
|
GetResource() schema.GroupVersionResource
|
||||||
|
// GetSubresource is the name of the subresource being requested. This is a different resource, scoped to the parent resource, but it may have a different kind.
|
||||||
|
// For instance, /pods has the resource "pods" and the kind "Pod", while /pods/foo/status has the resource "pods", the sub resource "status", and the kind "Pod"
|
||||||
|
// (because status operates on pods). The binding resource for a pod though may be /pods/foo/binding, which has resource "pods", subresource "binding", and kind "Binding".
|
||||||
|
GetSubresource() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAuthorizerVal(userInfo user.Info, authorizer authorizer.Authorizer) ref.Val {
|
||||||
|
return authorizerVal{receiverOnlyObjectVal: receiverOnlyVal(AuthorizerType), userInfo: userInfo, authAuthorizer: authorizer}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewResourceAuthorizerVal(userInfo user.Info, authorizer authorizer.Authorizer, requestResource Resource) ref.Val {
|
||||||
|
a := authorizerVal{receiverOnlyObjectVal: receiverOnlyVal(AuthorizerType), userInfo: userInfo, authAuthorizer: authorizer}
|
||||||
|
resource := requestResource.GetResource()
|
||||||
|
g := a.groupCheck(resource.Group)
|
||||||
|
r := g.resourceCheck(resource.Resource)
|
||||||
|
r.subresource = requestResource.GetSubresource()
|
||||||
|
r.namespace = requestResource.GetNamespace()
|
||||||
|
r.name = requestResource.GetName()
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
type authorizerVal struct {
|
||||||
|
receiverOnlyObjectVal
|
||||||
|
userInfo user.Info
|
||||||
|
authAuthorizer authorizer.Authorizer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a authorizerVal) pathCheck(path string) pathCheckVal {
|
||||||
|
return pathCheckVal{receiverOnlyObjectVal: receiverOnlyVal(PathCheckType), authorizer: a, path: path}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a authorizerVal) groupCheck(group string) groupCheckVal {
|
||||||
|
return groupCheckVal{receiverOnlyObjectVal: receiverOnlyVal(GroupCheckType), authorizer: a, group: group}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a authorizerVal) serviceAccount(namespace, name string) authorizerVal {
|
||||||
|
sa := &serviceaccount.ServiceAccountInfo{Name: name, Namespace: namespace}
|
||||||
|
return authorizerVal{
|
||||||
|
receiverOnlyObjectVal: receiverOnlyVal(AuthorizerType),
|
||||||
|
userInfo: sa.UserInfo(),
|
||||||
|
authAuthorizer: a.authAuthorizer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type pathCheckVal struct {
|
||||||
|
receiverOnlyObjectVal
|
||||||
|
authorizer authorizerVal
|
||||||
|
path string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a pathCheckVal) Authorize(ctx context.Context, verb string) ref.Val {
|
||||||
|
attr := &authorizer.AttributesRecord{
|
||||||
|
Path: a.path,
|
||||||
|
Verb: verb,
|
||||||
|
User: a.authorizer.userInfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
decision, reason, err := a.authorizer.authAuthorizer.Authorize(ctx, attr)
|
||||||
|
if err != nil {
|
||||||
|
return types.NewErr("error in authorization check: %v", err)
|
||||||
|
}
|
||||||
|
return newDecision(decision, reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
type groupCheckVal struct {
|
||||||
|
receiverOnlyObjectVal
|
||||||
|
authorizer authorizerVal
|
||||||
|
group string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g groupCheckVal) resourceCheck(resource string) resourceCheckVal {
|
||||||
|
return resourceCheckVal{receiverOnlyObjectVal: receiverOnlyVal(ResourceCheckType), groupCheck: g, resource: resource}
|
||||||
|
}
|
||||||
|
|
||||||
|
type resourceCheckVal struct {
|
||||||
|
receiverOnlyObjectVal
|
||||||
|
groupCheck groupCheckVal
|
||||||
|
resource string
|
||||||
|
subresource string
|
||||||
|
namespace string
|
||||||
|
name string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a resourceCheckVal) Authorize(ctx context.Context, verb string) ref.Val {
|
||||||
|
attr := &authorizer.AttributesRecord{
|
||||||
|
ResourceRequest: true,
|
||||||
|
APIGroup: a.groupCheck.group,
|
||||||
|
APIVersion: "*",
|
||||||
|
Resource: a.resource,
|
||||||
|
Subresource: a.subresource,
|
||||||
|
Namespace: a.namespace,
|
||||||
|
Name: a.name,
|
||||||
|
Verb: verb,
|
||||||
|
User: a.groupCheck.authorizer.userInfo,
|
||||||
|
}
|
||||||
|
decision, reason, err := a.groupCheck.authorizer.authAuthorizer.Authorize(ctx, attr)
|
||||||
|
if err != nil {
|
||||||
|
return types.NewErr("error in authorization check: %v", err)
|
||||||
|
}
|
||||||
|
return newDecision(decision, reason)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newDecision(authDecision authorizer.Decision, reason string) decisionVal {
|
||||||
|
return decisionVal{receiverOnlyObjectVal: receiverOnlyVal(DecisionType), authDecision: authDecision, reason: reason}
|
||||||
|
}
|
||||||
|
|
||||||
|
type decisionVal struct {
|
||||||
|
receiverOnlyObjectVal
|
||||||
|
authDecision authorizer.Decision
|
||||||
|
reason string
|
||||||
|
}
|
||||||
|
|
||||||
|
// receiverOnlyObjectVal provides an implementation of ref.Val for
|
||||||
|
// any object type that has receiver functions but does not expose any fields to
|
||||||
|
// CEL.
|
||||||
|
type receiverOnlyObjectVal struct {
|
||||||
|
typeValue *types.TypeValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// receiverOnlyVal returns a receiverOnlyObjectVal for the given type.
|
||||||
|
func receiverOnlyVal(objectType *cel.Type) receiverOnlyObjectVal {
|
||||||
|
return receiverOnlyObjectVal{typeValue: types.NewTypeValue(objectType.String())}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertToNative implements ref.Val.ConvertToNative.
|
||||||
|
func (a receiverOnlyObjectVal) ConvertToNative(typeDesc reflect.Type) (any, error) {
|
||||||
|
return nil, fmt.Errorf("type conversion error from '%s' to '%v'", a.typeValue.String(), typeDesc)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ConvertToType implements ref.Val.ConvertToType.
|
||||||
|
func (a receiverOnlyObjectVal) ConvertToType(typeVal ref.Type) ref.Val {
|
||||||
|
switch typeVal {
|
||||||
|
case a.typeValue:
|
||||||
|
return a
|
||||||
|
case types.TypeType:
|
||||||
|
return a.typeValue
|
||||||
|
}
|
||||||
|
return types.NewErr("type conversion error from '%s' to '%s'", a.typeValue, typeVal)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Equal implements ref.Val.Equal.
|
||||||
|
func (a receiverOnlyObjectVal) Equal(other ref.Val) ref.Val {
|
||||||
|
o, ok := other.(receiverOnlyObjectVal)
|
||||||
|
if !ok {
|
||||||
|
return types.MaybeNoSuchOverloadErr(other)
|
||||||
|
}
|
||||||
|
return types.Bool(a == o)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Type implements ref.Val.Type.
|
||||||
|
func (a receiverOnlyObjectVal) Type() ref.Type {
|
||||||
|
return a.typeValue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Value implements ref.Val.Value.
|
||||||
|
func (a receiverOnlyObjectVal) Value() any {
|
||||||
|
return types.NoSuchOverloadErr()
|
||||||
|
}
|
@ -36,6 +36,15 @@ type CostEstimator struct {
|
|||||||
|
|
||||||
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":
|
||||||
|
// An authorization check has a fixed cost
|
||||||
|
// This cost is set to allow for only two authorization checks per expression
|
||||||
|
cost := uint64(350000)
|
||||||
|
return &cost
|
||||||
|
case "serviceAccount", "path", "group", "resource", "subresource", "namespace", "name", "allowed", "denied", "reason":
|
||||||
|
// All authorization builder and accessor functions have a nominal cost
|
||||||
|
cost := uint64(1)
|
||||||
|
return &cost
|
||||||
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 {
|
||||||
@ -78,6 +87,13 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
|
|||||||
// WARNING: Any changes to this code impact API compatibility! The estimated cost is used to determine which CEL rules may be written to a
|
// WARNING: Any changes to this code impact API compatibility! The estimated cost is used to determine which CEL rules may be written to a
|
||||||
// CRD and any change (cost increases and cost decreases) are breaking.
|
// CRD and any change (cost increases and cost decreases) are breaking.
|
||||||
switch function {
|
switch function {
|
||||||
|
case "check":
|
||||||
|
// An authorization check has a fixed cost
|
||||||
|
// This cost is set to allow for only two authorization checks per expression
|
||||||
|
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 350000, Max: 350000}}
|
||||||
|
case "serviceAccount", "path", "group", "resource", "subresource", "namespace", "name", "allowed", "denied", "reason":
|
||||||
|
// All authorization builder and accessor functions have a nominal cost
|
||||||
|
return &checker.CallEstimate{CostEstimate: checker.CostEstimate{Min: 1, Max: 1}}
|
||||||
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
|
||||||
@ -94,7 +110,6 @@ func (l *CostEstimator) EstimateCallCost(function, overloadId string, target *ch
|
|||||||
} else { // the target is a string, which is supported by indexOf and lastIndexOf
|
} else { // the target is a string, which is supported by indexOf and lastIndexOf
|
||||||
return &checker.CallEstimate{CostEstimate: l.sizeEstimate(*target).MultiplyByCostFactor(common.StringTraversalCostFactor)}
|
return &checker.CallEstimate{CostEstimate: l.sizeEstimate(*target).MultiplyByCostFactor(common.StringTraversalCostFactor)}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
case "url":
|
case "url":
|
||||||
if len(args) == 1 {
|
if len(args) == 1 {
|
||||||
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||||||
package library
|
package library
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@ -24,6 +25,8 @@ import (
|
|||||||
"github.com/google/cel-go/checker"
|
"github.com/google/cel-go/checker"
|
||||||
"github.com/google/cel-go/ext"
|
"github.com/google/cel-go/ext"
|
||||||
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||||
|
|
||||||
|
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -311,12 +314,62 @@ func TestStringLibrary(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAuthzLibrary(t *testing.T) {
|
||||||
|
cases := []struct {
|
||||||
|
name string
|
||||||
|
expr string
|
||||||
|
expectEstimatedCost checker.CostEstimate
|
||||||
|
expectRuntimeCost uint64
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "path",
|
||||||
|
expr: "authorizer.path('/healthz')",
|
||||||
|
expectEstimatedCost: checker.CostEstimate{Min: 2, Max: 2},
|
||||||
|
expectRuntimeCost: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resource",
|
||||||
|
expr: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend')",
|
||||||
|
expectEstimatedCost: checker.CostEstimate{Min: 6, Max: 6},
|
||||||
|
expectRuntimeCost: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "path check allowed",
|
||||||
|
expr: "authorizer.path('/healthz').check('get').allowed()",
|
||||||
|
expectEstimatedCost: checker.CostEstimate{Min: 350003, Max: 350003},
|
||||||
|
expectRuntimeCost: 350003,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resource check allowed",
|
||||||
|
expr: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()",
|
||||||
|
expectEstimatedCost: checker.CostEstimate{Min: 350007, Max: 350007},
|
||||||
|
expectRuntimeCost: 350007,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "resource check reason",
|
||||||
|
expr: "authorizer.group('apps').resource('deployments').subresource('status').namespace('test').name('backend').check('create').allowed()",
|
||||||
|
expectEstimatedCost: checker.CostEstimate{Min: 350007, Max: 350007},
|
||||||
|
expectRuntimeCost: 350007,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range cases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
testCost(t, tc.expr, tc.expectEstimatedCost, tc.expectRuntimeCost)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate, expectRuntimeCost uint64) {
|
func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate, expectRuntimeCost uint64) {
|
||||||
est := &CostEstimator{SizeEstimator: &testCostEstimator{}}
|
est := &CostEstimator{SizeEstimator: &testCostEstimator{}}
|
||||||
env, err := cel.NewEnv(append(k8sExtensionLibs, ext.Strings())...)
|
env, err := cel.NewEnv(append(k8sExtensionLibs, ext.Strings())...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%v", err)
|
t.Fatalf("%v", err)
|
||||||
}
|
}
|
||||||
|
env, err = env.Extend(cel.Variable("authorizer", AuthorizerType))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("%v", err)
|
||||||
|
}
|
||||||
compiled, issues := env.Compile(expr)
|
compiled, issues := env.Compile(expr)
|
||||||
if len(issues.Errors()) > 0 {
|
if len(issues.Errors()) > 0 {
|
||||||
t.Fatalf("%v", issues.Errors())
|
t.Fatalf("%v", issues.Errors())
|
||||||
@ -332,7 +385,7 @@ func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%v", err)
|
t.Fatalf("%v", err)
|
||||||
}
|
}
|
||||||
_, details, err := prog.Eval(map[string]interface{}{})
|
_, details, err := prog.Eval(map[string]interface{}{"authorizer": NewAuthorizerVal(nil, alwaysAllowAuthorizer{})})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("%v", err)
|
t.Fatalf("%v", err)
|
||||||
}
|
}
|
||||||
@ -361,3 +414,9 @@ func (t *testCostEstimator) EstimateSize(element checker.AstNode) *checker.SizeE
|
|||||||
func (t *testCostEstimator) EstimateCallCost(function, overloadId string, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate {
|
func (t *testCostEstimator) EstimateCallCost(function, overloadId string, target *checker.AstNode, args []checker.AstNode) *checker.CallEstimate {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type alwaysAllowAuthorizer struct{}
|
||||||
|
|
||||||
|
func (f alwaysAllowAuthorizer) Authorize(ctx context.Context, a authorizer.Attributes) (authorizer.Decision, string, error) {
|
||||||
|
return authorizer.DecisionAllow, "", nil
|
||||||
|
}
|
||||||
|
@ -29,6 +29,7 @@ var k8sExtensionLibs = []cel.EnvOption{
|
|||||||
URLs(),
|
URLs(),
|
||||||
Regex(),
|
Regex(),
|
||||||
Lists(),
|
Lists(),
|
||||||
|
Authz(),
|
||||||
}
|
}
|
||||||
|
|
||||||
var ExtensionLibRegexOptimizations = []*interpreter.RegexOptimization{FindRegexOptimization, FindAllRegexOptimization}
|
var ExtensionLibRegexOptimizations = []*interpreter.RegexOptimization{FindRegexOptimization, FindAllRegexOptimization}
|
||||||
|
@ -29,6 +29,7 @@ func TestLibraryCompatibility(t *testing.T) {
|
|||||||
urlsLib: urlLibraryDecls,
|
urlsLib: urlLibraryDecls,
|
||||||
listsLib: listsLibraryDecls,
|
listsLib: listsLibraryDecls,
|
||||||
regexLib: regexLibraryDecls,
|
regexLib: regexLibraryDecls,
|
||||||
|
authzLib: authzLibraryDecls,
|
||||||
}
|
}
|
||||||
if len(k8sExtensionLibs) != len(decls) {
|
if len(k8sExtensionLibs) != len(decls) {
|
||||||
t.Errorf("Expected the same number of libraries in the ExtensionLibs as are tested for compatibility")
|
t.Errorf("Expected the same number of libraries in the ExtensionLibs as are tested for compatibility")
|
||||||
@ -46,6 +47,8 @@ func TestLibraryCompatibility(t *testing.T) {
|
|||||||
// Kubernetes 1.24:
|
// Kubernetes 1.24:
|
||||||
"isSorted", "sum", "max", "min", "indexOf", "lastIndexOf", "find", "findAll", "url", "getScheme", "getHost", "getHostname",
|
"isSorted", "sum", "max", "min", "indexOf", "lastIndexOf", "find", "findAll", "url", "getScheme", "getHost", "getHostname",
|
||||||
"getPort", "getEscapedPath", "getQuery", "isURL",
|
"getPort", "getEscapedPath", "getQuery", "isURL",
|
||||||
|
// Kubernetes <1.27>:
|
||||||
|
"path", "group", "serviceAccount", "resource", "subresource", "namespace", "name", "check", "allowed", "denied", "reason",
|
||||||
// Kubernetes <1.??>:
|
// Kubernetes <1.??>:
|
||||||
}
|
}
|
||||||
for _, fn := range knownFunctions {
|
for _, fn := range knownFunctions {
|
||||||
|
Loading…
Reference in New Issue
Block a user