Merge pull request #116054 from jpbetz/secondary-authz
KEP-3488: Implement secondary authz for ValidatingAdmissionPolicy Kubernetes-commit: 64259b43b8e5b7c086bf2b74743dc7e73ebe37e3
This commit is contained in:
commit
044de75c61
8
go.mod
8
go.mod
|
@ -42,9 +42,9 @@ require (
|
|||
google.golang.org/protobuf v1.28.1
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.0.0
|
||||
gopkg.in/square/go-jose.v2 v2.6.0
|
||||
k8s.io/api v0.0.0-20230304080250-2d949b7889c7
|
||||
k8s.io/api v0.0.0-20230306232903-507c3640d5c3
|
||||
k8s.io/apimachinery v0.0.0-20230303235435-f357b1fa74b7
|
||||
k8s.io/client-go v0.0.0-20230304000204-06ad6b391d35
|
||||
k8s.io/client-go v0.0.0-20230306193833-48ca73428a5d
|
||||
k8s.io/component-base v0.0.0-20230304000900-3bf8af940f29
|
||||
k8s.io/klog/v2 v2.90.1
|
||||
k8s.io/kms v0.0.0-20230304001132-5439f76ca4a7
|
||||
|
@ -124,9 +124,9 @@ require (
|
|||
)
|
||||
|
||||
replace (
|
||||
k8s.io/api => k8s.io/api v0.0.0-20230304080250-2d949b7889c7
|
||||
k8s.io/api => k8s.io/api v0.0.0-20230306232903-507c3640d5c3
|
||||
k8s.io/apimachinery => k8s.io/apimachinery v0.0.0-20230303235435-f357b1fa74b7
|
||||
k8s.io/client-go => k8s.io/client-go v0.0.0-20230304000204-06ad6b391d35
|
||||
k8s.io/client-go => k8s.io/client-go v0.0.0-20230306193833-48ca73428a5d
|
||||
k8s.io/component-base => k8s.io/component-base v0.0.0-20230304000900-3bf8af940f29
|
||||
k8s.io/kms => k8s.io/kms v0.0.0-20230304001132-5439f76ca4a7
|
||||
)
|
||||
|
|
8
go.sum
8
go.sum
|
@ -874,12 +874,12 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh
|
|||
honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg=
|
||||
honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k=
|
||||
k8s.io/api v0.0.0-20230304080250-2d949b7889c7 h1:aiAGgZyin08AS6FReGcsRu4Jx++Z2h1jgqG4wl6mho8=
|
||||
k8s.io/api v0.0.0-20230304080250-2d949b7889c7/go.mod h1:esKbT+6XB9TZUHyxlJVQ3zUM0abhQZ81Ic68eirO+xM=
|
||||
k8s.io/api v0.0.0-20230306232903-507c3640d5c3 h1:mIsULdBj0NtBvxyl7GiNPpt8yrpZhIEdWrx7XykA0vU=
|
||||
k8s.io/api v0.0.0-20230306232903-507c3640d5c3/go.mod h1:esKbT+6XB9TZUHyxlJVQ3zUM0abhQZ81Ic68eirO+xM=
|
||||
k8s.io/apimachinery v0.0.0-20230303235435-f357b1fa74b7 h1:YN43Lvs3Pj9iQmuWGojeBiFdz1mkrxe0EZn7Ba3TMpQ=
|
||||
k8s.io/apimachinery v0.0.0-20230303235435-f357b1fa74b7/go.mod h1:jlJwObMa4oKAEOMnAeEaqeiM+Fwd/CbAwNyQ7OaEwS0=
|
||||
k8s.io/client-go v0.0.0-20230304000204-06ad6b391d35 h1:9uwNzISbibeQwpbp/a9OFj2Zk98INi4pJfByXGK5nGQ=
|
||||
k8s.io/client-go v0.0.0-20230304000204-06ad6b391d35/go.mod h1:D9bGnWQ1292GhAR0f2uumanvVdOHddJlD+7OlyUorYM=
|
||||
k8s.io/client-go v0.0.0-20230306193833-48ca73428a5d h1:rSK2FCQrMNWOBZnZwuF5fjUVEhv6EYcno7svdgcOxhQ=
|
||||
k8s.io/client-go v0.0.0-20230306193833-48ca73428a5d/go.mod h1:bwEvXWHDzK2W1oimBN7Pr2YPWzAnkT9N/xiOOp7Znl8=
|
||||
k8s.io/component-base v0.0.0-20230304000900-3bf8af940f29 h1:26PuXs5/GsCX5EPltZN9KjZqkW/EwYHaebfNmIK3Wxc=
|
||||
k8s.io/component-base v0.0.0-20230304000900-3bf8af940f29/go.mod h1:1aFiUfjXiy45a88xJmd/nSkReVCoyShEHbGmWJQbp2Q=
|
||||
k8s.io/klog/v2 v2.90.1 h1:m4bYOKall2MmOiRaR1J+We67Do7vm9KiQVlT96lnHUw=
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package cel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
|
@ -26,43 +27,35 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
ObjectVarName = "object"
|
||||
OldObjectVarName = "oldObject"
|
||||
ParamsVarName = "params"
|
||||
RequestVarName = "request"
|
||||
ObjectVarName = "object"
|
||||
OldObjectVarName = "oldObject"
|
||||
ParamsVarName = "params"
|
||||
RequestVarName = "request"
|
||||
AuthorizerVarName = "authorizer"
|
||||
RequestResourceAuthorizerVarName = "authorizer.requestResource"
|
||||
|
||||
checkFrequency = 100
|
||||
)
|
||||
|
||||
type envs struct {
|
||||
noParams *cel.Env
|
||||
withParams *cel.Env
|
||||
}
|
||||
|
||||
var (
|
||||
initEnvsOnce sync.Once
|
||||
initEnvs *envs
|
||||
initEnvs envs
|
||||
initEnvsErr error
|
||||
)
|
||||
|
||||
func getEnvs() (*envs, error) {
|
||||
func getEnvs() (envs, error) {
|
||||
initEnvsOnce.Do(func() {
|
||||
base, err := buildBaseEnv()
|
||||
requiredVarsEnv, err := buildRequiredVarsEnv()
|
||||
if err != nil {
|
||||
initEnvsErr = err
|
||||
return
|
||||
}
|
||||
noParams, err := buildNoParamsEnv(base)
|
||||
|
||||
initEnvs, err = buildWithOptionalVarsEnvs(requiredVarsEnv)
|
||||
if err != nil {
|
||||
initEnvsErr = err
|
||||
return
|
||||
}
|
||||
withParams, err := buildWithParamsEnv(noParams)
|
||||
if err != nil {
|
||||
initEnvsErr = err
|
||||
return
|
||||
}
|
||||
initEnvs = &envs{noParams: noParams, withParams: withParams}
|
||||
})
|
||||
return initEnvs, initEnvsErr
|
||||
}
|
||||
|
@ -81,7 +74,11 @@ func buildBaseEnv() (*cel.Env, error) {
|
|||
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
|
||||
reg := apiservercel.NewRegistry(baseEnv)
|
||||
|
||||
|
@ -109,8 +106,33 @@ func buildNoParamsEnv(baseEnv *cel.Env) (*cel.Env, error) {
|
|||
return env, nil
|
||||
}
|
||||
|
||||
func buildWithParamsEnv(noParams *cel.Env) (*cel.Env, error) {
|
||||
return noParams.Extend(cel.Variable(ParamsVarName, cel.DynType))
|
||||
type envs map[OptionalVariableDeclarations]*cel.Env
|
||||
|
||||
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
|
||||
|
@ -168,7 +190,7 @@ type CompilationResult struct {
|
|||
}
|
||||
|
||||
// CompileCELExpression returns a compiled CEL expression.
|
||||
func CompileCELExpression(expressionAccessor ExpressionAccessor, hasParams bool) CompilationResult {
|
||||
func CompileCELExpression(expressionAccessor ExpressionAccessor, optionalVars OptionalVariableDeclarations) CompilationResult {
|
||||
var env *cel.Env
|
||||
envs, err := getEnvs()
|
||||
if err != nil {
|
||||
|
@ -180,10 +202,15 @@ func CompileCELExpression(expressionAccessor ExpressionAccessor, hasParams bool)
|
|||
ExpressionAccessor: expressionAccessor,
|
||||
}
|
||||
}
|
||||
if hasParams {
|
||||
env = envs.withParams
|
||||
} else {
|
||||
env = envs.noParams
|
||||
env, ok := envs[optionalVars]
|
||||
if !ok {
|
||||
return CompilationResult{
|
||||
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())
|
||||
|
|
|
@ -26,6 +26,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
|||
name string
|
||||
expressions []string
|
||||
hasParams bool
|
||||
hasAuthorizer bool
|
||||
errorExpressions map[string]string
|
||||
}{
|
||||
{
|
||||
|
@ -99,6 +100,19 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
|||
"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 {
|
||||
|
@ -106,7 +120,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
|||
for _, expr := range tc.expressions {
|
||||
result := CompileCELExpression(&fakeExpressionAccessor{
|
||||
expr,
|
||||
}, tc.hasParams)
|
||||
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: true})
|
||||
if result.Error != nil {
|
||||
t.Errorf("Unexpected error: %v", result.Error)
|
||||
}
|
||||
|
@ -114,7 +128,7 @@ func TestCompileValidatingPolicyExpression(t *testing.T) {
|
|||
for expr, expectErr := range tc.errorExpressions {
|
||||
result := CompileCELExpression(&fakeExpressionAccessor{
|
||||
expr,
|
||||
}, tc.hasParams)
|
||||
}, OptionalVariableDeclarations{HasParams: tc.hasParams, HasAuthorizer: tc.hasAuthorizer})
|
||||
if result.Error == nil {
|
||||
t.Errorf("Expected expression '%s' to contain '%v' but got no error", expr, expectErr)
|
||||
continue
|
||||
|
|
|
@ -31,6 +31,7 @@ import (
|
|||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||
"k8s.io/apiserver/pkg/cel/library"
|
||||
)
|
||||
|
||||
// filterCompiler implement the interface FilterCompiler.
|
||||
|
@ -42,7 +43,7 @@ func NewFilterCompiler() FilterCompiler {
|
|||
}
|
||||
|
||||
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
|
||||
|
@ -54,9 +55,13 @@ func (a *evaluationActivation) ResolveName(name string) (interface{}, bool) {
|
|||
case OldObjectVarName:
|
||||
return a.oldObject, true
|
||||
case ParamsVarName:
|
||||
return a.params, true
|
||||
return a.params, true // params may be null
|
||||
case RequestVarName:
|
||||
return a.request, true
|
||||
case AuthorizerVarName:
|
||||
return a.authorizer, a.authorizer != nil
|
||||
case RequestResourceAuthorizerVarName:
|
||||
return a.requestResourceAuthorizer, a.requestResourceAuthorizer != nil
|
||||
default:
|
||||
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
|
||||
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, hasParam bool) Filter {
|
||||
func (c *filterCompiler) Compile(expressionAccessors []ExpressionAccessor, options OptionalVariableDeclarations) Filter {
|
||||
if len(expressionAccessors) == 0 {
|
||||
return nil
|
||||
}
|
||||
compilationResults := make([]CompilationResult, len(expressionAccessors))
|
||||
for i, expressionAccessor := range expressionAccessors {
|
||||
compilationResults[i] = CompileCELExpression(expressionAccessor, hasParam)
|
||||
compilationResults[i] = CompileCELExpression(expressionAccessor, options)
|
||||
}
|
||||
return NewFilter(compilationResults)
|
||||
}
|
||||
|
@ -113,9 +118,9 @@ func objectToResolveVal(r runtime.Object) (interface{}, error) {
|
|||
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
|
||||
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
|
||||
evaluations := make([]EvaluationResult, len(f.compilationResults))
|
||||
var err error
|
||||
|
@ -128,9 +133,17 @@ func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, versionedP
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
paramsVal, err := objectToResolveVal(versionedParams)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
var paramsVal, authorizerVal, requestResourceAuthorizerVal any
|
||||
if inputs.VersionedParams != nil {
|
||||
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)
|
||||
|
@ -138,10 +151,12 @@ func (f *filter) ForInput(versionedAttr *generic.VersionedAttributes, versionedP
|
|||
return nil, err
|
||||
}
|
||||
va := &evaluationActivation{
|
||||
object: objectVal,
|
||||
oldObject: oldObjectVal,
|
||||
params: paramsVal,
|
||||
request: requestVal.Object,
|
||||
object: objectVal,
|
||||
oldObject: oldObjectVal,
|
||||
params: paramsVal,
|
||||
request: requestVal.Object,
|
||||
authorizer: authorizerVal,
|
||||
requestResourceAuthorizer: requestResourceAuthorizerVal,
|
||||
}
|
||||
|
||||
for i, compilationResult := range f.compilationResults {
|
||||
|
|
|
@ -17,12 +17,18 @@ limitations under the License.
|
|||
package cel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
celtypes "github.com/google/cel-go/common/types"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
|
@ -81,7 +87,7 @@ func TestCompile(t *testing.T) {
|
|||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
var c filterCompiler
|
||||
e := c.Compile(tc.validation, false)
|
||||
e := c.Compile(tc.validation, OptionalVariableDeclarations{HasParams: false, HasAuthorizer: false})
|
||||
if e == nil {
|
||||
t.Fatalf("unexpected nil validator")
|
||||
}
|
||||
|
@ -144,6 +150,7 @@ func TestFilter(t *testing.T) {
|
|||
validations []ExpressionAccessor
|
||||
results []EvaluationResult
|
||||
hasParamKind bool
|
||||
authorizer authorizer.Authorizer
|
||||
}{
|
||||
{
|
||||
name: "valid syntax for object",
|
||||
|
@ -417,12 +424,204 @@ func TestFilter(t *testing.T) {
|
|||
hasParamKind: true,
|
||||
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 {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
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 {
|
||||
t.Fatalf("unexpected nil validator")
|
||||
}
|
||||
|
@ -435,7 +634,8 @@ func TestFilter(t *testing.T) {
|
|||
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 {
|
||||
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"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
var _ ExpressionAccessor = &MatchCondition{}
|
||||
|
@ -49,19 +50,41 @@ func (v *MatchCondition) GetExpression() string {
|
|||
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.
|
||||
type FilterCompiler interface {
|
||||
// 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
|
||||
// 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).
|
||||
// versionedParams may be nil.
|
||||
type Filter interface {
|
||||
// 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() []error
|
||||
|
|
|
@ -23,6 +23,7 @@ import (
|
|||
"io"
|
||||
|
||||
"k8s.io/apimachinery/pkg/api/meta"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/component-base/featuregate"
|
||||
|
@ -71,6 +72,7 @@ type celAdmissionPlugin struct {
|
|||
restMapper meta.RESTMapper
|
||||
dynamicClient dynamic.Interface
|
||||
stopCh <-chan struct{}
|
||||
authorizer authorizer.Authorizer
|
||||
}
|
||||
|
||||
var _ initializer.WantsExternalKubeInformerFactory = &celAdmissionPlugin{}
|
||||
|
@ -78,6 +80,7 @@ var _ initializer.WantsExternalKubeClientSet = &celAdmissionPlugin{}
|
|||
var _ initializer.WantsRESTMapper = &celAdmissionPlugin{}
|
||||
var _ initializer.WantsDynamicClient = &celAdmissionPlugin{}
|
||||
var _ initializer.WantsDrainedNotification = &celAdmissionPlugin{}
|
||||
var _ initializer.WantsAuthorizer = &celAdmissionPlugin{}
|
||||
|
||||
var _ admission.InitializationValidator = &celAdmissionPlugin{}
|
||||
var _ admission.ValidationInterface = &celAdmissionPlugin{}
|
||||
|
@ -108,6 +111,10 @@ func (c *celAdmissionPlugin) SetDrainedNotification(stopCh <-chan struct{}) {
|
|||
c.stopCh = stopCh
|
||||
}
|
||||
|
||||
func (c *celAdmissionPlugin) SetAuthorizer(authorizer authorizer.Authorizer) {
|
||||
c.authorizer = authorizer
|
||||
}
|
||||
|
||||
func (c *celAdmissionPlugin) InspectFeatureGates(featureGates featuregate.FeatureGate) {
|
||||
if featureGates.Enabled(features.ValidatingAdmissionPolicy) {
|
||||
c.enabled = true
|
||||
|
@ -138,7 +145,10 @@ func (c *celAdmissionPlugin) ValidateInitialization() error {
|
|||
if c.stopCh == nil {
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -26,6 +26,8 @@ import (
|
|||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
admissionv1 "k8s.io/api/admission/v1"
|
||||
admissionRegistrationv1 "k8s.io/api/admissionregistration/v1"
|
||||
"k8s.io/api/admissionregistration/v1alpha1"
|
||||
|
@ -42,6 +44,7 @@ import (
|
|||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/validatingadmissionpolicy/internal/generic"
|
||||
whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
dynamicfake "k8s.io/client-go/dynamic/fake"
|
||||
"k8s.io/client-go/informers"
|
||||
|
@ -49,7 +52,6 @@ import (
|
|||
clienttesting "k8s.io/client-go/testing"
|
||||
"k8s.io/client-go/tools/cache"
|
||||
"k8s.io/component-base/featuregate"
|
||||
"k8s.io/klog/v2"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -158,7 +160,7 @@ var (
|
|||
// Interface which has fake compile functionality for use in tests
|
||||
// So that we can test the controller without pulling in any CEL functionality
|
||||
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{}
|
||||
|
@ -169,22 +171,22 @@ func (f *fakeCompiler) HasSynced() bool {
|
|||
|
||||
func (f *fakeCompiler) Compile(
|
||||
expressions []cel.ExpressionAccessor,
|
||||
hasParam bool,
|
||||
options cel.OptionalVariableDeclarations,
|
||||
) cel.Filter {
|
||||
key := expressions[0].GetExpression()
|
||||
if fun, ok := f.CompileFuncs[key]; ok {
|
||||
return fun(expressions, hasParam)
|
||||
return fun(expressions, options)
|
||||
}
|
||||
|
||||
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 := definition.Spec.Validations[0].Expression
|
||||
if compileFunc != 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
|
||||
}
|
||||
|
@ -206,7 +208,7 @@ type fakeFilter struct {
|
|||
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
|
||||
}
|
||||
|
||||
|
@ -333,6 +335,7 @@ func setupTestCommon(t *testing.T, compiler cel.FilterCompiler, matcher Matcher,
|
|||
testContext, testContextCancel := context.WithCancel(context.Background())
|
||||
t.Cleanup(testContextCancel)
|
||||
|
||||
fakeAuthorizer := fakeAuthorizer{}
|
||||
dynamicClient := dynamicfake.NewSimpleDynamicClient(scheme)
|
||||
|
||||
fakeClient := fake.NewSimpleClientset()
|
||||
|
@ -355,7 +358,7 @@ func setupTestCommon(t *testing.T, compiler cel.FilterCompiler, matcher Matcher,
|
|||
handler := plug.(*celAdmissionPlugin)
|
||||
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)
|
||||
handler.SetRESTMapper(fakeRestMapper)
|
||||
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
|
||||
controller = handler.evaluator.(*celAdmissionController)
|
||||
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)
|
||||
v := validatorMap[f.keyId]
|
||||
v.fakeFilter = f
|
||||
|
@ -702,7 +705,7 @@ func TestBasicPolicyDefinitionFailure(t *testing.T) {
|
|||
DefaultMatch: true,
|
||||
}
|
||||
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||
datalock.Lock()
|
||||
numCompiles += 1
|
||||
datalock.Unlock()
|
||||
|
@ -762,7 +765,7 @@ func TestDefinitionDoesntMatch(t *testing.T) {
|
|||
passedParams := []*unstructured.Unstructured{}
|
||||
numCompiles := 0
|
||||
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||
datalock.Lock()
|
||||
numCompiles += 1
|
||||
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()
|
||||
numCompiles += 1
|
||||
datalock.Unlock()
|
||||
|
@ -980,7 +983,7 @@ func TestRemoveDefinition(t *testing.T) {
|
|||
datalock := sync.Mutex{}
|
||||
numCompiles := 0
|
||||
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||
datalock.Lock()
|
||||
numCompiles += 1
|
||||
datalock.Unlock()
|
||||
|
@ -1047,7 +1050,7 @@ func TestRemoveBinding(t *testing.T) {
|
|||
datalock := sync.Mutex{}
|
||||
numCompiles := 0
|
||||
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||
datalock.Lock()
|
||||
numCompiles += 1
|
||||
datalock.Unlock()
|
||||
|
@ -1155,7 +1158,7 @@ func TestInvalidParamSourceInstanceName(t *testing.T) {
|
|||
passedParams := []*unstructured.Unstructured{}
|
||||
numCompiles := 0
|
||||
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
||||
compiler.RegisterDefinition(denyPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||
datalock.Lock()
|
||||
numCompiles += 1
|
||||
datalock.Unlock()
|
||||
|
@ -1221,7 +1224,7 @@ func TestEmptyParamSource(t *testing.T) {
|
|||
noParamSourcePolicy := *denyPolicy
|
||||
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()
|
||||
numCompiles += 1
|
||||
datalock.Unlock()
|
||||
|
@ -1323,7 +1326,7 @@ func TestMultiplePoliciesSharedParamType(t *testing.T) {
|
|||
compiles2 := 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)
|
||||
|
||||
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)
|
||||
|
||||
return &fakeFilter{
|
||||
|
@ -1448,7 +1451,7 @@ func TestNativeTypeParam(t *testing.T) {
|
|||
Kind: "ConfigMap",
|
||||
}
|
||||
|
||||
compiler.RegisterDefinition(&nativeTypeParamPolicy, func([]cel.ExpressionAccessor, bool) cel.Filter {
|
||||
compiler.RegisterDefinition(&nativeTypeParamPolicy, func([]cel.ExpressionAccessor, cel.OptionalVariableDeclarations) cel.Filter {
|
||||
compiles.Add(1)
|
||||
|
||||
return &fakeFilter{
|
||||
|
@ -1511,3 +1514,9 @@ func TestNativeTypeParam(t *testing.T) {
|
|||
require.EqualValues(t, 1, compiles.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/matching"
|
||||
whgeneric "k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/client-go/dynamic"
|
||||
"k8s.io/client-go/informers"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
|
@ -119,6 +120,7 @@ func NewAdmissionController(
|
|||
client kubernetes.Interface,
|
||||
restMapper meta.RESTMapper,
|
||||
dynamicClient dynamic.Interface,
|
||||
authz authorizer.Authorizer,
|
||||
) CELPolicyEvaluator {
|
||||
return &celAdmissionController{
|
||||
definitions: atomic.Value{},
|
||||
|
@ -132,6 +134,7 @@ func NewAdmissionController(
|
|||
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicies().Informer()),
|
||||
generic.NewInformer[*v1alpha1.ValidatingAdmissionPolicyBinding](
|
||||
informerFactory.Admissionregistration().V1alpha1().ValidatingAdmissionPolicyBindings().Informer()),
|
||||
authz,
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
|
@ -32,6 +32,7 @@ import (
|
|||
celmetrics "k8s.io/apiserver/pkg/admission/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"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/dynamicinformer"
|
||||
"k8s.io/client-go/informers"
|
||||
|
@ -85,9 +86,11 @@ type policyController struct {
|
|||
definitionsToBindings map[namespacedName]sets.Set[namespacedName]
|
||||
|
||||
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(
|
||||
restMapper meta.RESTMapper,
|
||||
|
@ -97,6 +100,7 @@ func newPolicyController(
|
|||
matcher Matcher,
|
||||
policiesInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicy],
|
||||
bindingsInformer generic.Informer[*v1alpha1.ValidatingAdmissionPolicyBinding],
|
||||
authz authorizer.Authorizer,
|
||||
) *policyController {
|
||||
res := &policyController{}
|
||||
*res = policyController{
|
||||
|
@ -126,6 +130,7 @@ func newPolicyController(
|
|||
restMapper: restMapper,
|
||||
dynamicClient: dynamicClient,
|
||||
client: client,
|
||||
authz: authz,
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
@ -439,9 +444,11 @@ func (c *policyController) latestPolicyData() []policyData {
|
|||
if definitionInfo.lastReconciledValue.Spec.ParamKind != nil {
|
||||
hasParam = true
|
||||
}
|
||||
optionalVars := cel.OptionalVariableDeclarations{HasParams: hasParam, HasAuthorizer: true}
|
||||
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),
|
||||
c.authz,
|
||||
)
|
||||
}
|
||||
bindingInfos = append(bindingInfos, *bindingInfo)
|
||||
|
|
|
@ -18,6 +18,7 @@ package validatingadmissionpolicy
|
|||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
)
|
||||
|
||||
|
|
|
@ -18,28 +18,31 @@ package validatingadmissionpolicy
|
|||
|
||||
import (
|
||||
"fmt"
|
||||
"k8s.io/klog/v2"
|
||||
"strings"
|
||||
|
||||
celtypes "github.com/google/cel-go/common/types"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
v1 "k8s.io/api/admissionregistration/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/webhook/generic"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
// validator implements the Validator interface
|
||||
type validator struct {
|
||||
filter cel.Filter
|
||||
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{
|
||||
filter: filter,
|
||||
failPolicy: failPolicy,
|
||||
authorizer: authorizer,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -59,7 +62,8 @@ func (v *validator) Validate(versionedAttr *generic.VersionedAttributes, version
|
|||
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 {
|
||||
return []PolicyDecision{
|
||||
{
|
||||
|
|
|
@ -28,7 +28,6 @@ import (
|
|||
admissionv1 "k8s.io/api/admission/v1"
|
||||
v1 "k8s.io/api/admissionregistration/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apiserver/pkg/admission"
|
||||
"k8s.io/apiserver/pkg/admission/plugin/cel"
|
||||
|
@ -42,7 +41,7 @@ type fakeCelFilter struct {
|
|||
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 {
|
||||
return nil, errors.New("test error")
|
||||
}
|
||||
|
@ -465,6 +464,7 @@ func TestValidate(t *testing.T) {
|
|||
throwError: tc.throwError,
|
||||
},
|
||||
}
|
||||
|
||||
policyResults := v.Validate(fakeVersionedAttr, nil)
|
||||
|
||||
require.Equal(t, len(policyResults), len(tc.policyDecision))
|
||||
|
|
|
@ -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 {
|
||||
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":
|
||||
var cost uint64
|
||||
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
|
||||
// CRD and any change (cost increases and cost decreases) are breaking.
|
||||
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":
|
||||
if target != nil {
|
||||
// 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
|
||||
return &checker.CallEstimate{CostEstimate: l.sizeEstimate(*target).MultiplyByCostFactor(common.StringTraversalCostFactor)}
|
||||
}
|
||||
|
||||
}
|
||||
case "url":
|
||||
if len(args) == 1 {
|
||||
|
|
|
@ -17,6 +17,7 @@ limitations under the License.
|
|||
package library
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
|
@ -24,6 +25,8 @@ import (
|
|||
"github.com/google/cel-go/checker"
|
||||
"github.com/google/cel-go/ext"
|
||||
expr "google.golang.org/genproto/googleapis/api/expr/v1alpha1"
|
||||
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
|
||||
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) {
|
||||
est := &CostEstimator{SizeEstimator: &testCostEstimator{}}
|
||||
env, err := cel.NewEnv(append(k8sExtensionLibs, ext.Strings())...)
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
env, err = env.Extend(cel.Variable("authorizer", AuthorizerType))
|
||||
if err != nil {
|
||||
t.Fatalf("%v", err)
|
||||
}
|
||||
compiled, issues := env.Compile(expr)
|
||||
if len(issues.Errors()) > 0 {
|
||||
t.Fatalf("%v", issues.Errors())
|
||||
|
@ -332,7 +385,7 @@ func testCost(t *testing.T, expr string, expectEsimatedCost checker.CostEstimate
|
|||
if err != nil {
|
||||
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 {
|
||||
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 {
|
||||
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(),
|
||||
Regex(),
|
||||
Lists(),
|
||||
Authz(),
|
||||
}
|
||||
|
||||
var ExtensionLibRegexOptimizations = []*interpreter.RegexOptimization{FindRegexOptimization, FindAllRegexOptimization}
|
||||
|
|
|
@ -29,6 +29,7 @@ func TestLibraryCompatibility(t *testing.T) {
|
|||
urlsLib: urlLibraryDecls,
|
||||
listsLib: listsLibraryDecls,
|
||||
regexLib: regexLibraryDecls,
|
||||
authzLib: authzLibraryDecls,
|
||||
}
|
||||
if len(k8sExtensionLibs) != len(decls) {
|
||||
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:
|
||||
"isSorted", "sum", "max", "min", "indexOf", "lastIndexOf", "find", "findAll", "url", "getScheme", "getHost", "getHostname",
|
||||
"getPort", "getEscapedPath", "getQuery", "isURL",
|
||||
// Kubernetes <1.27>:
|
||||
"path", "group", "serviceAccount", "resource", "subresource", "namespace", "name", "check", "allowed", "denied", "reason",
|
||||
// Kubernetes <1.??>:
|
||||
}
|
||||
for _, fn := range knownFunctions {
|
||||
|
|
Loading…
Reference in New Issue