mirror of
https://github.com/k3s-io/kubernetes.git
synced 2025-07-31 15:25:57 +00:00
Merge pull request #121223 from ritazh/authz-cel
[StructuredAuthorizationConfig] - CEL integration
This commit is contained in:
commit
064e86b3d0
@ -19,6 +19,7 @@ package authorizer
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
utilnet "k8s.io/apimachinery/pkg/util/net"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
authzconfig "k8s.io/apiserver/pkg/apis/apiserver"
|
||||
@ -122,6 +123,7 @@ func (config Config) New() (authorizer.Authorizer, authorizer.RuleResolver, erro
|
||||
configuredAuthorizer.Webhook.AuthorizedTTL.Duration,
|
||||
configuredAuthorizer.Webhook.UnauthorizedTTL.Duration,
|
||||
*config.WebhookRetryBackoff,
|
||||
configuredAuthorizer.Webhook.MatchConditions,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
@ -17,8 +17,8 @@ limitations under the License.
|
||||
package validation
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@ -27,10 +27,15 @@ import (
|
||||
|
||||
v1 "k8s.io/api/authorization/v1"
|
||||
"k8s.io/api/authorization/v1beta1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
utilvalidation "k8s.io/apimachinery/pkg/util/validation"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/apiserver/pkg/apis/apiserver"
|
||||
authorizationcel "k8s.io/apiserver/pkg/authorization/cel"
|
||||
"k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/client-go/util/cert"
|
||||
)
|
||||
|
||||
@ -334,24 +339,81 @@ func ValidateWebhookConfiguration(fldPath *field.Path, c *api.WebhookConfigurati
|
||||
allErrs = append(allErrs, field.NotSupported(fldPath.Child("connectionInfo", "type"), c.ConnectionInfo, []string{api.AuthorizationWebhookConnectionInfoTypeInCluster, api.AuthorizationWebhookConnectionInfoTypeKubeConfigFile}))
|
||||
}
|
||||
|
||||
// TODO: Remove this check and ensure that correct validations below for MatchConditions are added
|
||||
// for i, condition := range c.MatchConditions {
|
||||
// fldPath := fldPath.Child("matchConditions").Index(i).Child("expression")
|
||||
// if len(strings.TrimSpace(condition.Expression)) == 0 {
|
||||
// allErrs = append(allErrs, field.Required(fldPath, ""))
|
||||
// } else {
|
||||
// allErrs = append(allErrs, ValidateWebhookMatchCondition(fldPath, sampleSAR, condition.Expression)...)
|
||||
// }
|
||||
// }
|
||||
if len(c.MatchConditions) != 0 {
|
||||
allErrs = append(allErrs, field.NotSupported(fldPath.Child("matchConditions"), c.MatchConditions, []string{}))
|
||||
_, errs := compileMatchConditions(c.MatchConditions, fldPath, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration))
|
||||
allErrs = append(allErrs, errs...)
|
||||
|
||||
return allErrs
|
||||
}
|
||||
|
||||
// ValidateAndCompileMatchConditions validates a given webhook's matchConditions.
|
||||
// This is exported for use in authz package.
|
||||
func ValidateAndCompileMatchConditions(matchConditions []api.WebhookMatchCondition) (*authorizationcel.CELMatcher, field.ErrorList) {
|
||||
return compileMatchConditions(matchConditions, nil, utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration))
|
||||
}
|
||||
|
||||
func compileMatchConditions(matchConditions []api.WebhookMatchCondition, fldPath *field.Path, structuredAuthzFeatureEnabled bool) (*authorizationcel.CELMatcher, field.ErrorList) {
|
||||
var allErrs field.ErrorList
|
||||
// should fail when match conditions are used without feature enabled
|
||||
if len(matchConditions) > 0 && !structuredAuthzFeatureEnabled {
|
||||
allErrs = append(allErrs, field.Invalid(fldPath.Child("matchConditions"), "", "matchConditions are not supported when StructuredAuthorizationConfiguration feature gate is disabled"))
|
||||
}
|
||||
if len(matchConditions) > 64 {
|
||||
allErrs = append(allErrs, field.TooMany(fldPath.Child("matchConditions"), len(matchConditions), 64))
|
||||
return nil, allErrs
|
||||
}
|
||||
|
||||
return allErrs
|
||||
compiler := authorizationcel.NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
||||
seenExpressions := sets.NewString()
|
||||
var compilationResults []authorizationcel.CompilationResult
|
||||
|
||||
for i, condition := range matchConditions {
|
||||
fldPath := fldPath.Child("matchConditions").Index(i).Child("expression")
|
||||
if len(strings.TrimSpace(condition.Expression)) == 0 {
|
||||
allErrs = append(allErrs, field.Required(fldPath, ""))
|
||||
continue
|
||||
}
|
||||
if seenExpressions.Has(condition.Expression) {
|
||||
allErrs = append(allErrs, field.Duplicate(fldPath, condition.Expression))
|
||||
continue
|
||||
}
|
||||
seenExpressions.Insert(condition.Expression)
|
||||
compilationResult, err := compileMatchConditionsExpression(fldPath, compiler, condition.Expression)
|
||||
if err != nil {
|
||||
allErrs = append(allErrs, err)
|
||||
continue
|
||||
}
|
||||
compilationResults = append(compilationResults, compilationResult)
|
||||
}
|
||||
if len(compilationResults) == 0 {
|
||||
return nil, allErrs
|
||||
}
|
||||
return &authorizationcel.CELMatcher{
|
||||
CompilationResults: compilationResults,
|
||||
}, allErrs
|
||||
}
|
||||
|
||||
func ValidateWebhookMatchCondition(fldPath *field.Path, sampleSAR runtime.Object, expression string) field.ErrorList {
|
||||
allErrs := field.ErrorList{}
|
||||
// TODO: typecheck CEL expression
|
||||
return allErrs
|
||||
func compileMatchConditionsExpression(fldPath *field.Path, compiler authorizationcel.Compiler, expression string) (authorizationcel.CompilationResult, *field.Error) {
|
||||
authzExpression := &authorizationcel.SubjectAccessReviewMatchCondition{
|
||||
Expression: expression,
|
||||
}
|
||||
compilationResult, err := compiler.CompileCELExpression(authzExpression)
|
||||
if err != nil {
|
||||
return compilationResult, convertCELErrorToValidationError(fldPath, authzExpression, err)
|
||||
}
|
||||
return compilationResult, nil
|
||||
}
|
||||
|
||||
func convertCELErrorToValidationError(fldPath *field.Path, expression authorizationcel.ExpressionAccessor, err error) *field.Error {
|
||||
var celErr *cel.Error
|
||||
if errors.As(err, &celErr) {
|
||||
switch celErr.Type {
|
||||
case cel.ErrorTypeRequired:
|
||||
return field.Required(fldPath, celErr.Detail)
|
||||
case cel.ErrorTypeInvalid:
|
||||
return field.Invalid(fldPath, expression.GetExpression(), celErr.Detail)
|
||||
default:
|
||||
return field.InternalError(fldPath, celErr)
|
||||
}
|
||||
}
|
||||
return field.InternalError(fldPath, fmt.Errorf("error is not cel error: %w", err))
|
||||
}
|
||||
|
@ -32,7 +32,10 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/validation/field"
|
||||
api "k8s.io/apiserver/pkg/apis/apiserver"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
certutil "k8s.io/client-go/util/cert"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
"k8s.io/utils/pointer"
|
||||
)
|
||||
|
||||
@ -428,6 +431,8 @@ type (
|
||||
)
|
||||
|
||||
func TestValidateAuthorizationConfiguration(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, true)()
|
||||
|
||||
badKubeConfigFile := "../some/relative/path/kubeconfig"
|
||||
|
||||
tempKubeConfigFile, err := os.CreateTemp("/tmp", "kubeconfig")
|
||||
@ -557,6 +562,39 @@ func TestValidateAuthorizationConfiguration(t *testing.T) {
|
||||
knownTypes: sets.NewString(string("Webhook")),
|
||||
repeatableTypes: sets.NewString(string("Webhook")),
|
||||
},
|
||||
{
|
||||
name: "bare minimum configuration with Webhook and MatchConditions",
|
||||
configuration: api.AuthorizationConfiguration{
|
||||
Authorizers: []api.AuthorizerConfiguration{
|
||||
{
|
||||
Type: "Webhook",
|
||||
Name: "default",
|
||||
Webhook: &api.WebhookConfiguration{
|
||||
Timeout: metav1.Duration{Duration: 5 * time.Second},
|
||||
AuthorizedTTL: metav1.Duration{Duration: 5 * time.Minute},
|
||||
UnauthorizedTTL: metav1.Duration{Duration: 30 * time.Second},
|
||||
FailurePolicy: "NoOpinion",
|
||||
SubjectAccessReviewVersion: "v1",
|
||||
MatchConditionSubjectAccessReviewVersion: "v1",
|
||||
ConnectionInfo: api.WebhookConnectionInfo{
|
||||
Type: "InClusterConfig",
|
||||
},
|
||||
MatchConditions: []api.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kube-system'",
|
||||
},
|
||||
{
|
||||
Expression: "request.user == 'admin'",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
expectedErrList: field.ErrorList{},
|
||||
knownTypes: sets.NewString(string("Webhook")),
|
||||
repeatableTypes: sets.NewString(string("Webhook")),
|
||||
},
|
||||
{
|
||||
name: "bare minimum configuration with multiple webhooks",
|
||||
configuration: api.AuthorizationConfiguration{
|
||||
@ -1156,8 +1194,6 @@ func TestValidateAuthorizationConfiguration(t *testing.T) {
|
||||
knownTypes: sets.NewString(string("Webhook")),
|
||||
repeatableTypes: sets.NewString(string("Webhook")),
|
||||
},
|
||||
|
||||
// TODO: When the CEL expression validator is implemented, add a few test cases to typecheck the expression
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
@ -1166,20 +1202,120 @@ func TestValidateAuthorizationConfiguration(t *testing.T) {
|
||||
if len(errList) != len(test.expectedErrList) {
|
||||
t.Errorf("expected %d errs, got %d, errors %v", len(test.expectedErrList), len(errList), errList)
|
||||
}
|
||||
|
||||
for i, expected := range test.expectedErrList {
|
||||
if expected.Type.String() != errList[i].Type.String() {
|
||||
t.Errorf("expected err type %s, got %s",
|
||||
expected.Type.String(),
|
||||
errList[i].Type.String())
|
||||
}
|
||||
if expected.BadValue != errList[i].BadValue {
|
||||
t.Errorf("expected bad value '%s', got '%s'",
|
||||
expected.BadValue,
|
||||
errList[i].BadValue)
|
||||
if len(errList) == len(test.expectedErrList) {
|
||||
for i, expected := range test.expectedErrList {
|
||||
if expected.Type.String() != errList[i].Type.String() {
|
||||
t.Errorf("expected err type %s, got %s",
|
||||
expected.Type.String(),
|
||||
errList[i].Type.String())
|
||||
}
|
||||
if expected.BadValue != errList[i].BadValue {
|
||||
t.Errorf("expected bad value '%s', got '%s'",
|
||||
expected.BadValue,
|
||||
errList[i].BadValue)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateAndCompileMatchConditions(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
matchConditions []api.WebhookMatchCondition
|
||||
featureEnabled bool
|
||||
expectedErr string
|
||||
}{
|
||||
{
|
||||
name: "match conditions are used With feature enabled",
|
||||
matchConditions: []api.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kube-system'",
|
||||
},
|
||||
{
|
||||
Expression: "request.user == 'admin'",
|
||||
},
|
||||
},
|
||||
featureEnabled: true,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "should fail when match conditions are used without feature enabled",
|
||||
matchConditions: []api.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kube-system'",
|
||||
},
|
||||
{
|
||||
Expression: "request.user == 'admin'",
|
||||
},
|
||||
},
|
||||
featureEnabled: false,
|
||||
expectedErr: `matchConditions: Invalid value: "": matchConditions are not supported when StructuredAuthorizationConfiguration feature gate is disabled`,
|
||||
},
|
||||
{
|
||||
name: "no matchConditions should not require feature enablement",
|
||||
matchConditions: []api.WebhookMatchCondition{},
|
||||
featureEnabled: false,
|
||||
expectedErr: "",
|
||||
},
|
||||
{
|
||||
name: "match conditions with invalid expressions",
|
||||
matchConditions: []api.WebhookMatchCondition{
|
||||
{
|
||||
Expression: " ",
|
||||
},
|
||||
},
|
||||
featureEnabled: true,
|
||||
expectedErr: "matchConditions[0].expression: Required value",
|
||||
},
|
||||
{
|
||||
name: "match conditions with duplicate expressions",
|
||||
matchConditions: []api.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "request.user == 'admin'",
|
||||
},
|
||||
{
|
||||
Expression: "request.user == 'admin'",
|
||||
},
|
||||
},
|
||||
featureEnabled: true,
|
||||
expectedErr: `matchConditions[1].expression: Duplicate value: "request.user == 'admin'"`,
|
||||
},
|
||||
{
|
||||
name: "match conditions with undeclared reference",
|
||||
matchConditions: []api.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "test",
|
||||
},
|
||||
},
|
||||
featureEnabled: true,
|
||||
expectedErr: "matchConditions[0].expression: Invalid value: \"test\": compilation failed: ERROR: <input>:1:1: undeclared reference to 'test' (in container '')\n | test\n | ^",
|
||||
},
|
||||
{
|
||||
name: "match conditions with bad return type",
|
||||
matchConditions: []api.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "request.user = 'test'",
|
||||
},
|
||||
},
|
||||
featureEnabled: true,
|
||||
expectedErr: "matchConditions[0].expression: Invalid value: \"request.user = 'test'\": compilation failed: ERROR: <input>:1:14: Syntax error: token recognition error at: '= '\n | request.user = 'test'\n | .............^\nERROR: <input>:1:16: Syntax error: extraneous input ''test'' expecting <EOF>\n | request.user = 'test'\n | ...............^",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range testCases {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, tt.featureEnabled)()
|
||||
celMatcher, errList := ValidateAndCompileMatchConditions(tt.matchConditions)
|
||||
if len(tt.expectedErr) == 0 && len(tt.matchConditions) > 0 && len(errList) == 0 && celMatcher == nil {
|
||||
t.Errorf("celMatcher should not be nil when there are matchCondition and no error returned")
|
||||
}
|
||||
got := errList.ToAggregate()
|
||||
if d := cmp.Diff(tt.expectedErr, errString(got)); d != "" {
|
||||
t.Fatalf("ValidateAndCompileMatchConditions validation mismatch (-want +got):\n%s", d)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
178
staging/src/k8s.io/apiserver/pkg/authorization/cel/compile.go
Normal file
178
staging/src/k8s.io/apiserver/pkg/authorization/cel/compile.go
Normal file
@ -0,0 +1,178 @@
|
||||
/*
|
||||
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 cel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/google/cel-go/cel"
|
||||
"github.com/google/cel-go/common/types/ref"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/version"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
)
|
||||
|
||||
const (
|
||||
subjectAccessReviewRequestVarName = "request"
|
||||
)
|
||||
|
||||
// CompilationResult represents a compiled authorization cel expression.
|
||||
type CompilationResult struct {
|
||||
Program cel.Program
|
||||
ExpressionAccessor ExpressionAccessor
|
||||
}
|
||||
|
||||
// EvaluationResult contains the minimal required fields and metadata of a cel evaluation
|
||||
type EvaluationResult struct {
|
||||
EvalResult ref.Val
|
||||
ExpressionAccessor ExpressionAccessor
|
||||
}
|
||||
|
||||
// Compiler is an interface for compiling CEL expressions with the desired environment mode.
|
||||
type Compiler interface {
|
||||
CompileCELExpression(expressionAccessor ExpressionAccessor) (CompilationResult, error)
|
||||
}
|
||||
|
||||
type compiler struct {
|
||||
envSet *environment.EnvSet
|
||||
}
|
||||
|
||||
// NewCompiler returns a new Compiler.
|
||||
func NewCompiler(env *environment.EnvSet) Compiler {
|
||||
return &compiler{
|
||||
envSet: mustBuildEnv(env),
|
||||
}
|
||||
}
|
||||
|
||||
func (c compiler) CompileCELExpression(expressionAccessor ExpressionAccessor) (CompilationResult, error) {
|
||||
resultError := func(errorString string, errType apiservercel.ErrorType) (CompilationResult, error) {
|
||||
err := &apiservercel.Error{
|
||||
Type: errType,
|
||||
Detail: errorString,
|
||||
}
|
||||
return CompilationResult{
|
||||
ExpressionAccessor: expressionAccessor,
|
||||
}, err
|
||||
}
|
||||
env, err := c.envSet.Env(environment.StoredExpressions)
|
||||
if err != nil {
|
||||
return resultError(fmt.Sprintf("unexpected error loading CEL environment: %v", err), apiservercel.ErrorTypeInternal)
|
||||
}
|
||||
ast, issues := env.Compile(expressionAccessor.GetExpression())
|
||||
if issues != nil {
|
||||
return resultError("compilation failed: "+issues.String(), apiservercel.ErrorTypeInvalid)
|
||||
}
|
||||
found := false
|
||||
returnTypes := expressionAccessor.ReturnTypes()
|
||||
for _, returnType := range returnTypes {
|
||||
if ast.OutputType() == returnType {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
var reason string
|
||||
if len(returnTypes) == 1 {
|
||||
reason = fmt.Sprintf("must evaluate to %v but got %v", returnTypes[0].String(), ast.OutputType())
|
||||
} else {
|
||||
reason = fmt.Sprintf("must evaluate to one of %v", returnTypes)
|
||||
}
|
||||
|
||||
return resultError(reason, apiservercel.ErrorTypeInvalid)
|
||||
}
|
||||
_, err = cel.AstToCheckedExpr(ast)
|
||||
if err != nil {
|
||||
// should be impossible since env.Compile returned no issues
|
||||
return resultError("unexpected compilation error: "+err.Error(), apiservercel.ErrorTypeInternal)
|
||||
}
|
||||
prog, err := env.Program(ast)
|
||||
if err != nil {
|
||||
return resultError("program instantiation failed: "+err.Error(), apiservercel.ErrorTypeInternal)
|
||||
}
|
||||
return CompilationResult{
|
||||
Program: prog,
|
||||
ExpressionAccessor: expressionAccessor,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func mustBuildEnv(baseEnv *environment.EnvSet) *environment.EnvSet {
|
||||
field := func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField {
|
||||
return apiservercel.NewDeclField(name, declType, required, nil, nil)
|
||||
}
|
||||
fields := func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField {
|
||||
result := make(map[string]*apiservercel.DeclField, len(fields))
|
||||
for _, f := range fields {
|
||||
result[f.Name] = f
|
||||
}
|
||||
return result
|
||||
}
|
||||
subjectAccessReviewSpecRequestType := buildRequestType(field, fields)
|
||||
extended, err := baseEnv.Extend(
|
||||
environment.VersionedOptions{
|
||||
// we record this as 1.0 since it was available in the
|
||||
// first version that supported this feature
|
||||
IntroducedVersion: version.MajorMinor(1, 0),
|
||||
EnvOptions: []cel.EnvOption{
|
||||
cel.Variable(subjectAccessReviewRequestVarName, subjectAccessReviewSpecRequestType.CelType()),
|
||||
},
|
||||
DeclTypes: []*apiservercel.DeclType{
|
||||
subjectAccessReviewSpecRequestType,
|
||||
},
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("environment misconfigured: %v", err))
|
||||
}
|
||||
|
||||
return extended
|
||||
}
|
||||
|
||||
// buildRequestType generates a DeclType for SubjectAccessReviewSpec.
|
||||
func buildRequestType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType {
|
||||
resourceAttributesType := buildResourceAttributesType(field, fields)
|
||||
nonResourceAttributesType := buildNonResourceAttributesType(field, fields)
|
||||
return apiservercel.NewObjectType("kubernetes.SubjectAccessReviewSpec", fields(
|
||||
field("resourceAttributes", resourceAttributesType, false),
|
||||
field("nonResourceAttributes", nonResourceAttributesType, false),
|
||||
field("user", apiservercel.StringType, false),
|
||||
field("groups", apiservercel.NewListType(apiservercel.StringType, -1), false),
|
||||
field("extra", apiservercel.NewMapType(apiservercel.StringType, apiservercel.NewListType(apiservercel.StringType, -1), -1), false),
|
||||
field("uid", apiservercel.StringType, false),
|
||||
))
|
||||
}
|
||||
|
||||
// buildResourceAttributesType generates a DeclType for ResourceAttributes.
|
||||
func buildResourceAttributesType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType {
|
||||
return apiservercel.NewObjectType("kubernetes.ResourceAttributes", fields(
|
||||
field("namespace", apiservercel.StringType, false),
|
||||
field("verb", apiservercel.StringType, false),
|
||||
field("group", apiservercel.StringType, false),
|
||||
field("version", apiservercel.StringType, false),
|
||||
field("resource", apiservercel.StringType, false),
|
||||
field("subresource", apiservercel.StringType, false),
|
||||
field("name", apiservercel.StringType, false),
|
||||
))
|
||||
}
|
||||
|
||||
// buildNonResourceAttributesType generates a DeclType for NonResourceAttributes.
|
||||
func buildNonResourceAttributesType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType {
|
||||
return apiservercel.NewObjectType("kubernetes.NonResourceAttributes", fields(
|
||||
field("path", apiservercel.StringType, false),
|
||||
field("verb", apiservercel.StringType, false),
|
||||
))
|
||||
}
|
@ -0,0 +1,158 @@
|
||||
/*
|
||||
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 cel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
v1 "k8s.io/api/authorization/v1"
|
||||
apiservercel "k8s.io/apiserver/pkg/cel"
|
||||
"k8s.io/apiserver/pkg/cel/environment"
|
||||
)
|
||||
|
||||
func TestCompileCELExpression(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
expression string
|
||||
expectedError string
|
||||
}{
|
||||
{
|
||||
name: "SubjectAccessReviewSpec user comparison",
|
||||
expression: "request.user == 'bob'",
|
||||
},
|
||||
{
|
||||
name: "undefined fields",
|
||||
expression: "request.time == 'now'",
|
||||
expectedError: "undefined field",
|
||||
},
|
||||
{
|
||||
name: "Syntax errors",
|
||||
expression: "request++'",
|
||||
expectedError: "Syntax error",
|
||||
},
|
||||
{
|
||||
name: "bad return type",
|
||||
expression: "request.user",
|
||||
expectedError: "must evaluate to bool",
|
||||
},
|
||||
{
|
||||
name: "undeclared reference",
|
||||
expression: "x.user",
|
||||
expectedError: "undeclared reference",
|
||||
},
|
||||
}
|
||||
|
||||
compiler := NewCompiler(environment.MustBaseEnvSet(environment.DefaultCompatibilityVersion()))
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := compiler.CompileCELExpression(&SubjectAccessReviewMatchCondition{
|
||||
Expression: tc.expression,
|
||||
})
|
||||
if len(tc.expectedError) > 0 && (err == nil || !strings.Contains(err.Error(), tc.expectedError)) {
|
||||
t.Fatalf("expected error: %s compiling expression %s, got: %v", tc.expectedError, tc.expression, err)
|
||||
}
|
||||
if len(tc.expectedError) == 0 && err != nil {
|
||||
t.Fatalf("unexpected error %v compiling expression %s", err, tc.expression)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRequestType(t *testing.T) {
|
||||
f := func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField {
|
||||
return apiservercel.NewDeclField(name, declType, required, nil, nil)
|
||||
}
|
||||
fs := func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField {
|
||||
result := make(map[string]*apiservercel.DeclField, len(fields))
|
||||
for _, f := range fields {
|
||||
result[f.Name] = f
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
requestDeclType := buildRequestType(f, fs)
|
||||
requestType := reflect.TypeOf(v1.SubjectAccessReviewSpec{})
|
||||
if len(requestDeclType.Fields) != requestType.NumField() {
|
||||
t.Fatalf("expected %d fields for SubjectAccessReviewSpec, got %d", requestType.NumField(), len(requestDeclType.Fields))
|
||||
}
|
||||
resourceAttributesDeclType := buildResourceAttributesType(f, fs)
|
||||
resourceAttributeType := reflect.TypeOf(v1.ResourceAttributes{})
|
||||
if len(resourceAttributesDeclType.Fields) != resourceAttributeType.NumField() {
|
||||
t.Fatalf("expected %d fields for ResourceAttributes, got %d", resourceAttributeType.NumField(), len(resourceAttributesDeclType.Fields))
|
||||
}
|
||||
nonResourceAttributesDeclType := buildNonResourceAttributesType(f, fs)
|
||||
nonResourceAttributeType := reflect.TypeOf(v1.NonResourceAttributes{})
|
||||
if len(nonResourceAttributesDeclType.Fields) != nonResourceAttributeType.NumField() {
|
||||
t.Fatalf("expected %d fields for NonResourceAttributes, got %d", nonResourceAttributeType.NumField(), len(nonResourceAttributesDeclType.Fields))
|
||||
}
|
||||
if err := compareFieldsForType(t, requestType, requestDeclType, f, fs); err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func compareFieldsForType(t *testing.T, nativeType reflect.Type, declType *apiservercel.DeclType, field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) error {
|
||||
for i := 0; i < nativeType.NumField(); i++ {
|
||||
nativeField := nativeType.Field(i)
|
||||
jsonTagParts := strings.Split(nativeField.Tag.Get("json"), ",")
|
||||
if len(jsonTagParts) < 1 {
|
||||
t.Fatal("expected json tag to be present")
|
||||
}
|
||||
fieldName := jsonTagParts[0]
|
||||
|
||||
declField, ok := declType.Fields[fieldName]
|
||||
if !ok {
|
||||
t.Fatalf("expected field %q to be present", nativeField.Name)
|
||||
}
|
||||
declFieldType := nativeTypeToCELType(t, nativeField.Type, field, fields)
|
||||
if declFieldType != nil && declFieldType.CelType().Equal(declField.Type.CelType()).Value() != true {
|
||||
return fmt.Errorf("expected native field %q to have type %v, got %v", nativeField.Name, nativeField.Type, declField.Type)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func nativeTypeToCELType(t *testing.T, nativeType reflect.Type, field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType {
|
||||
switch nativeType {
|
||||
case reflect.TypeOf(""):
|
||||
return apiservercel.StringType
|
||||
case reflect.TypeOf([]string{}):
|
||||
return apiservercel.NewListType(apiservercel.StringType, -1)
|
||||
case reflect.TypeOf(map[string]v1.ExtraValue{}):
|
||||
return apiservercel.NewMapType(apiservercel.StringType, apiservercel.NewListType(apiservercel.StringType, -1), -1)
|
||||
case reflect.TypeOf(&v1.ResourceAttributes{}):
|
||||
resourceAttributesDeclType := buildResourceAttributesType(field, fields)
|
||||
if err := compareFieldsForType(t, reflect.TypeOf(v1.ResourceAttributes{}), resourceAttributesDeclType, field, fields); err != nil {
|
||||
t.Error(err)
|
||||
return nil
|
||||
}
|
||||
return resourceAttributesDeclType
|
||||
case reflect.TypeOf(&v1.NonResourceAttributes{}):
|
||||
nonResourceAttributesDeclType := buildNonResourceAttributesType(field, fields)
|
||||
if err := compareFieldsForType(t, reflect.TypeOf(v1.NonResourceAttributes{}), nonResourceAttributesDeclType, field, fields); err != nil {
|
||||
t.Error(err)
|
||||
return nil
|
||||
}
|
||||
return nonResourceAttributesDeclType
|
||||
default:
|
||||
t.Fatalf("unsupported type %v", nativeType)
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,41 @@
|
||||
/*
|
||||
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 cel
|
||||
|
||||
import (
|
||||
celgo "github.com/google/cel-go/cel"
|
||||
)
|
||||
|
||||
type ExpressionAccessor interface {
|
||||
GetExpression() string
|
||||
ReturnTypes() []*celgo.Type
|
||||
}
|
||||
|
||||
var _ ExpressionAccessor = &SubjectAccessReviewMatchCondition{}
|
||||
|
||||
// SubjectAccessReviewMatchCondition is a CEL expression that maps a SubjectAccessReview request to a list of values.
|
||||
type SubjectAccessReviewMatchCondition struct {
|
||||
Expression string
|
||||
}
|
||||
|
||||
func (v *SubjectAccessReviewMatchCondition) GetExpression() string {
|
||||
return v.Expression
|
||||
}
|
||||
|
||||
func (v *SubjectAccessReviewMatchCondition) ReturnTypes() []*celgo.Type {
|
||||
return []*celgo.Type{celgo.BoolType}
|
||||
}
|
@ -0,0 +1,81 @@
|
||||
/*
|
||||
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 cel
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
celgo "github.com/google/cel-go/cel"
|
||||
authorizationv1 "k8s.io/api/authorization/v1"
|
||||
"k8s.io/apimachinery/pkg/runtime"
|
||||
utilerrors "k8s.io/apimachinery/pkg/util/errors"
|
||||
)
|
||||
|
||||
type CELMatcher struct {
|
||||
CompilationResults []CompilationResult
|
||||
}
|
||||
|
||||
// eval evaluates the given SubjectAccessReview against all cel matchCondition expression
|
||||
func (c *CELMatcher) Eval(ctx context.Context, r *authorizationv1.SubjectAccessReview) (bool, error) {
|
||||
var evalErrors []error
|
||||
specValObject, err := convertObjectToUnstructured(&r.Spec)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("authz celMatcher eval error: convert SubjectAccessReviewSpec object to unstructured failed: %w", err)
|
||||
}
|
||||
va := map[string]interface{}{
|
||||
"request": specValObject,
|
||||
}
|
||||
for _, compilationResult := range c.CompilationResults {
|
||||
evalResult, _, err := compilationResult.Program.ContextEval(ctx, va)
|
||||
if err != nil {
|
||||
evalErrors = append(evalErrors, fmt.Errorf("cel evaluation error: expression '%v' resulted in error: %w", compilationResult.ExpressionAccessor.GetExpression(), err))
|
||||
continue
|
||||
}
|
||||
if evalResult.Type() != celgo.BoolType {
|
||||
evalErrors = append(evalErrors, fmt.Errorf("cel evaluation error: expression '%v' eval result type should be bool but got %W", compilationResult.ExpressionAccessor.GetExpression(), evalResult.Type()))
|
||||
continue
|
||||
}
|
||||
match, ok := evalResult.Value().(bool)
|
||||
if !ok {
|
||||
evalErrors = append(evalErrors, fmt.Errorf("cel evaluation error: expression '%v' eval result value should be bool but got %W", compilationResult.ExpressionAccessor.GetExpression(), evalResult.Value()))
|
||||
continue
|
||||
}
|
||||
// If at least one matchCondition successfully evaluates to FALSE,
|
||||
// return early
|
||||
if !match {
|
||||
return false, nil
|
||||
}
|
||||
}
|
||||
// if there is any error, return
|
||||
if len(evalErrors) > 0 {
|
||||
return false, utilerrors.NewAggregate(evalErrors)
|
||||
}
|
||||
// return ALL matchConditions evaluate to TRUE successfully without error
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func convertObjectToUnstructured(obj *authorizationv1.SubjectAccessReviewSpec) (map[string]interface{}, error) {
|
||||
if obj == nil {
|
||||
return nil, nil
|
||||
}
|
||||
ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return ret, nil
|
||||
}
|
@ -20,6 +20,7 @@ import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apiserver/pkg/apis/apiserver"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
)
|
||||
@ -79,7 +80,7 @@ func TestAuthorizerMetrics(t *testing.T) {
|
||||
RecordRequestTotal: fakeAuthzMetrics.RequestTotal,
|
||||
RecordRequestLatency: fakeAuthzMetrics.RequestLatency,
|
||||
}
|
||||
wh, err := newV1Authorizer(server.URL, scenario.clientCert, scenario.clientKey, scenario.clientCA, 0, authzMetrics)
|
||||
wh, err := newV1Authorizer(server.URL, scenario.clientCert, scenario.clientKey, scenario.clientCA, 0, authzMetrics, []apiserver.WebhookMatchCondition{})
|
||||
if err != nil {
|
||||
t.Error("failed to create client")
|
||||
return
|
||||
|
@ -31,8 +31,13 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/apimachinery/pkg/util/cache"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/apis/apiserver"
|
||||
apiservervalidation "k8s.io/apiserver/pkg/apis/apiserver/validation"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
authorizationcel "k8s.io/apiserver/pkg/authorization/cel"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
"k8s.io/apiserver/pkg/util/webhook"
|
||||
"k8s.io/client-go/kubernetes/scheme"
|
||||
authorizationv1client "k8s.io/client-go/kubernetes/typed/authorization/v1"
|
||||
@ -66,11 +71,12 @@ type WebhookAuthorizer struct {
|
||||
retryBackoff wait.Backoff
|
||||
decisionOnError authorizer.Decision
|
||||
metrics AuthorizerMetrics
|
||||
celMatcher *authorizationcel.CELMatcher
|
||||
}
|
||||
|
||||
// NewFromInterface creates a WebhookAuthorizer using the given subjectAccessReview client
|
||||
func NewFromInterface(subjectAccessReview authorizationv1client.AuthorizationV1Interface, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, metrics AuthorizerMetrics) (*WebhookAuthorizer, error) {
|
||||
return newWithBackoff(&subjectAccessReviewV1Client{subjectAccessReview.RESTClient()}, authorizedTTL, unauthorizedTTL, retryBackoff, metrics)
|
||||
return newWithBackoff(&subjectAccessReviewV1Client{subjectAccessReview.RESTClient()}, authorizedTTL, unauthorizedTTL, retryBackoff, nil, metrics)
|
||||
}
|
||||
|
||||
// New creates a new WebhookAuthorizer from the provided kubeconfig file.
|
||||
@ -92,19 +98,24 @@ func NewFromInterface(subjectAccessReview authorizationv1client.AuthorizationV1I
|
||||
//
|
||||
// For additional HTTP configuration, refer to the kubeconfig documentation
|
||||
// https://kubernetes.io/docs/user-guide/kubeconfig-file/.
|
||||
func New(config *rest.Config, version string, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff) (*WebhookAuthorizer, error) {
|
||||
func New(config *rest.Config, version string, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, matchConditions []apiserver.WebhookMatchCondition) (*WebhookAuthorizer, error) {
|
||||
subjectAccessReview, err := subjectAccessReviewInterfaceFromConfig(config, version, retryBackoff)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return newWithBackoff(subjectAccessReview, authorizedTTL, unauthorizedTTL, retryBackoff, AuthorizerMetrics{
|
||||
return newWithBackoff(subjectAccessReview, authorizedTTL, unauthorizedTTL, retryBackoff, matchConditions, AuthorizerMetrics{
|
||||
RecordRequestTotal: noopMetrics{}.RecordRequestTotal,
|
||||
RecordRequestLatency: noopMetrics{}.RecordRequestLatency,
|
||||
})
|
||||
}
|
||||
|
||||
// newWithBackoff allows tests to skip the sleep.
|
||||
func newWithBackoff(subjectAccessReview subjectAccessReviewer, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, metrics AuthorizerMetrics) (*WebhookAuthorizer, error) {
|
||||
func newWithBackoff(subjectAccessReview subjectAccessReviewer, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, matchConditions []apiserver.WebhookMatchCondition, metrics AuthorizerMetrics) (*WebhookAuthorizer, error) {
|
||||
// compile all expressions once in validation and save the results to be used for eval later
|
||||
cm, fieldErr := apiservervalidation.ValidateAndCompileMatchConditions(matchConditions)
|
||||
if err := fieldErr.ToAggregate(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &WebhookAuthorizer{
|
||||
subjectAccessReview: subjectAccessReview,
|
||||
responseCache: cache.NewLRUExpireCache(8192),
|
||||
@ -113,6 +124,7 @@ func newWithBackoff(subjectAccessReview subjectAccessReviewer, authorizedTTL, un
|
||||
retryBackoff: retryBackoff,
|
||||
decisionOnError: authorizer.DecisionNoOpinion,
|
||||
metrics: metrics,
|
||||
celMatcher: cm,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -190,6 +202,24 @@ func (w *WebhookAuthorizer) Authorize(ctx context.Context, attr authorizer.Attri
|
||||
Verb: attr.GetVerb(),
|
||||
}
|
||||
}
|
||||
// skipping match when feature is not enabled
|
||||
if utilfeature.DefaultFeatureGate.Enabled(features.StructuredAuthorizationConfiguration) {
|
||||
// Process Match Conditions before calling the webhook
|
||||
matches, err := w.match(ctx, r)
|
||||
// If at least one matchCondition evaluates to an error (but none are FALSE):
|
||||
// If failurePolicy=Deny, then the webhook rejects the request
|
||||
// If failurePolicy=NoOpinion, then the error is ignored and the webhook is skipped
|
||||
if err != nil {
|
||||
return w.decisionOnError, "", err
|
||||
}
|
||||
// If at least one matchCondition successfully evaluates to FALSE,
|
||||
// then the webhook is skipped.
|
||||
if !matches {
|
||||
return authorizer.DecisionNoOpinion, "", nil
|
||||
}
|
||||
}
|
||||
// If all evaluated successfully and ALL matchConditions evaluate to TRUE,
|
||||
// then the webhook is called.
|
||||
key, err := json.Marshal(r.Spec)
|
||||
if err != nil {
|
||||
return w.decisionOnError, "", err
|
||||
@ -256,6 +286,18 @@ func (w *WebhookAuthorizer) RulesFor(user user.Info, namespace string) ([]author
|
||||
return resourceRules, nonResourceRules, incomplete, fmt.Errorf("webhook authorizer does not support user rule resolution")
|
||||
}
|
||||
|
||||
// Match is used to evaluate the SubjectAccessReviewSpec against
|
||||
// the authorizer's matchConditions in the form of cel expressions
|
||||
// to return match or no match found, which then is used to
|
||||
// determine if the webhook should be skipped.
|
||||
func (w *WebhookAuthorizer) match(ctx context.Context, r *authorizationv1.SubjectAccessReview) (bool, error) {
|
||||
// A nil celMatcher or zero saved CompilationResults matches all requests.
|
||||
if w.celMatcher == nil || w.celMatcher.CompilationResults == nil {
|
||||
return true, nil
|
||||
}
|
||||
return w.celMatcher.Eval(ctx, r)
|
||||
}
|
||||
|
||||
func convertToSARExtra(extra map[string][]string) map[string]authorizationv1.ExtraValue {
|
||||
if extra == nil {
|
||||
return nil
|
||||
|
@ -40,10 +40,14 @@ import (
|
||||
authorizationv1 "k8s.io/api/authorization/v1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apimachinery/pkg/util/wait"
|
||||
"k8s.io/apiserver/pkg/apis/apiserver"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
"k8s.io/apiserver/pkg/features"
|
||||
utilfeature "k8s.io/apiserver/pkg/util/feature"
|
||||
webhookutil "k8s.io/apiserver/pkg/util/webhook"
|
||||
v1 "k8s.io/client-go/tools/clientcmd/api/v1"
|
||||
featuregatetesting "k8s.io/component-base/featuregate/testing"
|
||||
)
|
||||
|
||||
var testRetryBackoff = wait.Backoff{
|
||||
@ -205,7 +209,7 @@ current-context: default
|
||||
if err != nil {
|
||||
return fmt.Errorf("error building sar client: %v", err)
|
||||
}
|
||||
_, err = newWithBackoff(sarClient, 0, 0, testRetryBackoff, noopAuthorizerMetrics())
|
||||
_, err = newWithBackoff(sarClient, 0, 0, testRetryBackoff, []apiserver.WebhookMatchCondition{}, noopAuthorizerMetrics())
|
||||
return err
|
||||
}()
|
||||
if err != nil && !tt.wantErr {
|
||||
@ -318,7 +322,7 @@ func (m *mockV1Service) HTTPStatusCode() int { return m.statusCode }
|
||||
|
||||
// newV1Authorizer creates a temporary kubeconfig file from the provided arguments and attempts to load
|
||||
// a new WebhookAuthorizer from it.
|
||||
func newV1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, metrics AuthorizerMetrics) (*WebhookAuthorizer, error) {
|
||||
func newV1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cacheTime time.Duration, metrics AuthorizerMetrics, expressions []apiserver.WebhookMatchCondition) (*WebhookAuthorizer, error) {
|
||||
tempfile, err := ioutil.TempFile("", "")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -348,7 +352,7 @@ func newV1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cache
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error building sar client: %v", err)
|
||||
}
|
||||
return newWithBackoff(sarClient, cacheTime, cacheTime, testRetryBackoff, metrics)
|
||||
return newWithBackoff(sarClient, cacheTime, cacheTime, testRetryBackoff, expressions, metrics)
|
||||
}
|
||||
|
||||
func TestV1TLSConfig(t *testing.T) {
|
||||
@ -407,7 +411,7 @@ func TestV1TLSConfig(t *testing.T) {
|
||||
}
|
||||
defer server.Close()
|
||||
|
||||
wh, err := newV1Authorizer(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0, noopAuthorizerMetrics())
|
||||
wh, err := newV1Authorizer(server.URL, tt.clientCert, tt.clientKey, tt.clientCA, 0, noopAuthorizerMetrics(), []apiserver.WebhookMatchCondition{})
|
||||
if err != nil {
|
||||
t.Errorf("%s: failed to create client: %v", tt.test, err)
|
||||
return
|
||||
@ -472,7 +476,7 @@ func TestV1Webhook(t *testing.T) {
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics())
|
||||
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), []apiserver.WebhookMatchCondition{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@ -572,15 +576,20 @@ func TestV1WebhookCache(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, true)()
|
||||
expressions := []apiserver.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'",
|
||||
},
|
||||
}
|
||||
// Create an authorizer that caches successful responses "forever" (100 days).
|
||||
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 2400*time.Hour, noopAuthorizerMetrics())
|
||||
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 2400*time.Hour, noopAuthorizerMetrics(), expressions)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
aliceAttr := authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "alice"}}
|
||||
bobAttr := authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "bob"}}
|
||||
aliceAttr := authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "alice"}, ResourceRequest: true, Namespace: "kittensandponies"}
|
||||
bobAttr := authorizer.AttributesRecord{User: &user.DefaultInfo{Name: "bob"}, ResourceRequest: true, Namespace: "kittensandponies"}
|
||||
aliceRidiculousAttr := authorizer.AttributesRecord{
|
||||
User: &user.DefaultInfo{Name: "alice"},
|
||||
ResourceRequest: true,
|
||||
@ -589,6 +598,7 @@ func TestV1WebhookCache(t *testing.T) {
|
||||
APIVersion: strings.Repeat("a", 2000),
|
||||
Resource: strings.Repeat("r", 2000),
|
||||
Name: strings.Repeat("n", 2000),
|
||||
Namespace: "kittensandponies",
|
||||
}
|
||||
bobRidiculousAttr := authorizer.AttributesRecord{
|
||||
User: &user.DefaultInfo{Name: "bob"},
|
||||
@ -598,6 +608,7 @@ func TestV1WebhookCache(t *testing.T) {
|
||||
APIVersion: strings.Repeat("a", 2000),
|
||||
Resource: strings.Repeat("r", 2000),
|
||||
Name: strings.Repeat("n", 2000),
|
||||
Namespace: "kittensandponies",
|
||||
}
|
||||
|
||||
type webhookCacheTestCase struct {
|
||||
@ -665,6 +676,404 @@ func TestV1WebhookCache(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestStructuredAuthzConfigFeatureEnablement verifies cel expressions can only be used when feature is enabled
|
||||
func TestStructuredAuthzConfigFeatureEnablement(t *testing.T) {
|
||||
|
||||
service := new(mockV1Service)
|
||||
service.statusCode = 200
|
||||
service.Allow()
|
||||
s, err := NewV1TestServer(service, serverCert, serverKey, caCert)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
type webhookMatchConditionsTestCase struct {
|
||||
name string
|
||||
attr authorizer.AttributesRecord
|
||||
allow bool
|
||||
expectedCompileErr bool
|
||||
expectedEvalErr bool
|
||||
expectedDecision authorizer.Decision
|
||||
expressions []apiserver.WebhookMatchCondition
|
||||
featureEnabled bool
|
||||
}
|
||||
aliceAttr := authorizer.AttributesRecord{
|
||||
User: &user.DefaultInfo{
|
||||
Name: "alice",
|
||||
UID: "1",
|
||||
Groups: []string{"group1", "group2"},
|
||||
},
|
||||
ResourceRequest: true,
|
||||
Namespace: "kittensandponies",
|
||||
Verb: "get",
|
||||
}
|
||||
tests := []webhookMatchConditionsTestCase{
|
||||
{
|
||||
name: "no match condition does not require feature enablement",
|
||||
attr: aliceAttr,
|
||||
allow: true,
|
||||
expectedCompileErr: false,
|
||||
expectedDecision: authorizer.DecisionAllow,
|
||||
expressions: []apiserver.WebhookMatchCondition{},
|
||||
featureEnabled: false,
|
||||
},
|
||||
{
|
||||
name: "should fail when match conditions are used without feature enabled",
|
||||
attr: aliceAttr,
|
||||
allow: false,
|
||||
expectedCompileErr: true,
|
||||
expectedDecision: authorizer.DecisionNoOpinion,
|
||||
expressions: []apiserver.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "request.user == 'alice'",
|
||||
},
|
||||
},
|
||||
featureEnabled: false,
|
||||
},
|
||||
{
|
||||
name: "feature enabled, match all against all expressions",
|
||||
attr: aliceAttr,
|
||||
allow: true,
|
||||
expectedCompileErr: false,
|
||||
expectedDecision: authorizer.DecisionAllow,
|
||||
expressions: []apiserver.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "request.user == 'alice'",
|
||||
},
|
||||
{
|
||||
Expression: "request.uid == '1'",
|
||||
},
|
||||
{
|
||||
Expression: "('group1' in request.groups)",
|
||||
},
|
||||
{
|
||||
Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'",
|
||||
},
|
||||
},
|
||||
featureEnabled: true,
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, test.featureEnabled)()
|
||||
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), test.expressions)
|
||||
if test.expectedCompileErr && err == nil {
|
||||
t.Fatalf("%d: Expected compile error", i)
|
||||
} else if !test.expectedCompileErr && err != nil {
|
||||
t.Fatalf("%d: unexpected error when creating a new WebhookAuthorizer: %v", i, err)
|
||||
}
|
||||
if err == nil {
|
||||
authorized, _, err := wh.Authorize(context.Background(), test.attr)
|
||||
if test.expectedEvalErr && err == nil {
|
||||
t.Fatalf("%d: Expected eval error", i)
|
||||
} else if !test.expectedEvalErr && err != nil {
|
||||
t.Fatalf("%d: unexpected error when authorizing: %v", i, err)
|
||||
}
|
||||
|
||||
if test.expectedDecision != authorized {
|
||||
t.Errorf("%d: expected authorized=%v, got %v", i, test.expectedDecision, authorized)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestV1WebhookMatchConditions verifies cel expressions are compiled and evaluated correctly
|
||||
func TestV1WebhookMatchConditions(t *testing.T) {
|
||||
defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StructuredAuthorizationConfiguration, true)()
|
||||
service := new(mockV1Service)
|
||||
service.statusCode = 200
|
||||
service.Allow()
|
||||
s, err := NewV1TestServer(service, serverCert, serverKey, caCert)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer s.Close()
|
||||
|
||||
aliceAttr := authorizer.AttributesRecord{
|
||||
User: &user.DefaultInfo{
|
||||
Name: "alice",
|
||||
UID: "1",
|
||||
Groups: []string{"group1", "group2"},
|
||||
},
|
||||
ResourceRequest: true,
|
||||
Namespace: "kittensandponies",
|
||||
Verb: "get",
|
||||
}
|
||||
bobAttr := authorizer.AttributesRecord{
|
||||
User: &user.DefaultInfo{
|
||||
Name: "bob",
|
||||
},
|
||||
ResourceRequest: false,
|
||||
Namespace: "kittensandponies",
|
||||
Verb: "get",
|
||||
}
|
||||
alice2Attr := authorizer.AttributesRecord{
|
||||
User: &user.DefaultInfo{
|
||||
Name: "alice2",
|
||||
},
|
||||
}
|
||||
type webhookMatchConditionsTestCase struct {
|
||||
name string
|
||||
attr authorizer.AttributesRecord
|
||||
expectedCompileErr string
|
||||
expectedEvalErr string
|
||||
expectedDecision authorizer.Decision
|
||||
expressions []apiserver.WebhookMatchCondition
|
||||
}
|
||||
|
||||
tests := []webhookMatchConditionsTestCase{
|
||||
{
|
||||
name: "match all with no expressions",
|
||||
attr: aliceAttr,
|
||||
expectedCompileErr: "",
|
||||
expectedDecision: authorizer.DecisionAllow,
|
||||
expressions: []apiserver.WebhookMatchCondition{},
|
||||
},
|
||||
{
|
||||
name: "match all against all expressions",
|
||||
attr: aliceAttr,
|
||||
expectedCompileErr: "",
|
||||
expectedDecision: authorizer.DecisionAllow,
|
||||
expressions: []apiserver.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "request.user == 'alice'",
|
||||
},
|
||||
{
|
||||
Expression: "request.uid == '1'",
|
||||
},
|
||||
{
|
||||
Expression: "('group1' in request.groups)",
|
||||
},
|
||||
{
|
||||
Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "match all except group, eval to one successful false, no error",
|
||||
attr: aliceAttr,
|
||||
expectedCompileErr: "",
|
||||
expectedDecision: authorizer.DecisionNoOpinion,
|
||||
expectedEvalErr: "",
|
||||
expressions: []apiserver.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "request.user == 'alice'",
|
||||
},
|
||||
{
|
||||
Expression: "request.uid == '1'",
|
||||
},
|
||||
{
|
||||
Expression: "('group3' in request.groups)",
|
||||
},
|
||||
{
|
||||
Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "match condition with one compilation error",
|
||||
attr: aliceAttr,
|
||||
expectedCompileErr: "matchConditions[2].expression: Invalid value: \"('group3' in request.group)\": compilation failed: ERROR: <input>:1:21: undefined field 'group'\n | ('group3' in request.group)\n | ....................^",
|
||||
expectedDecision: authorizer.DecisionNoOpinion,
|
||||
expressions: []apiserver.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "request.user == 'alice'",
|
||||
},
|
||||
{
|
||||
Expression: "request.uid == '1'",
|
||||
},
|
||||
{
|
||||
Expression: "('group3' in request.group)",
|
||||
},
|
||||
{
|
||||
Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "match all except uid",
|
||||
attr: aliceAttr,
|
||||
expectedCompileErr: "",
|
||||
expectedDecision: authorizer.DecisionNoOpinion,
|
||||
expressions: []apiserver.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "request.user == 'alice'",
|
||||
},
|
||||
{
|
||||
Expression: "request.uid == '2'",
|
||||
},
|
||||
{
|
||||
Expression: "('group1' in request.groups)",
|
||||
},
|
||||
{
|
||||
Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "match on user name but not namespace",
|
||||
attr: aliceAttr,
|
||||
expectedCompileErr: "",
|
||||
expectedDecision: authorizer.DecisionNoOpinion,
|
||||
expressions: []apiserver.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "request.user == 'alice'",
|
||||
},
|
||||
{
|
||||
Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kube-system'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "mismatch on user name",
|
||||
attr: bobAttr,
|
||||
expectedCompileErr: "",
|
||||
expectedDecision: authorizer.DecisionNoOpinion,
|
||||
expressions: []apiserver.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "request.user == 'alice'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "match on user name but not resourceAttributes",
|
||||
attr: bobAttr,
|
||||
expectedCompileErr: "",
|
||||
expectedDecision: authorizer.DecisionNoOpinion,
|
||||
expressions: []apiserver.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "request.user == 'bob'",
|
||||
},
|
||||
{
|
||||
Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "expression failed to compile due to wrong return type",
|
||||
attr: bobAttr,
|
||||
expectedCompileErr: `matchConditions[0].expression: Invalid value: "request.user": must evaluate to bool but got string`,
|
||||
expectedDecision: authorizer.DecisionNoOpinion,
|
||||
expressions: []apiserver.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "request.user",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "eval failed due to errors, no successful fail",
|
||||
attr: alice2Attr,
|
||||
expectedCompileErr: "",
|
||||
expectedEvalErr: "cel evaluation error: expression 'request.resourceAttributes.namespace == 'kittensandponies'' resulted in error: no such key: resourceAttributes",
|
||||
expectedDecision: authorizer.DecisionNoOpinion,
|
||||
expressions: []apiserver.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "request.user == 'alice2'",
|
||||
},
|
||||
{
|
||||
Expression: "request.resourceAttributes.namespace == 'kittensandponies'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "at least one matchCondition successfully evaluates to FALSE, error ignored",
|
||||
attr: alice2Attr,
|
||||
expectedCompileErr: "",
|
||||
expectedEvalErr: "",
|
||||
expectedDecision: authorizer.DecisionNoOpinion,
|
||||
expressions: []apiserver.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "request.user != 'alice2'",
|
||||
},
|
||||
{
|
||||
Expression: "request.resourceAttributes.namespace == 'kittensandponies'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "match on user name but failed to compile due to type check in nonResourceAttributes",
|
||||
attr: bobAttr,
|
||||
expectedCompileErr: "matchConditions[1].expression: Invalid value: \"request.nonResourceAttributes.verb == 2\": compilation failed: ERROR: <input>:1:36: found no matching overload for '_==_' applied to '(string, int)'\n | request.nonResourceAttributes.verb == 2\n | ...................................^",
|
||||
expectedDecision: authorizer.DecisionNoOpinion,
|
||||
expressions: []apiserver.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "request.user == 'bob'",
|
||||
},
|
||||
{
|
||||
Expression: "request.nonResourceAttributes.verb == 2",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "match on user name and nonresourceAttributes",
|
||||
attr: bobAttr,
|
||||
expectedCompileErr: "",
|
||||
expectedDecision: authorizer.DecisionAllow,
|
||||
expressions: []apiserver.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "request.user == 'bob'",
|
||||
},
|
||||
{
|
||||
Expression: "has(request.nonResourceAttributes) && request.nonResourceAttributes.verb == 'get'",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "match eval failed with bad SubjectAccessReviewSpec",
|
||||
attr: authorizer.AttributesRecord{},
|
||||
expectedCompileErr: "",
|
||||
// default decisionOnError in newWithBackoff to skip
|
||||
expectedDecision: authorizer.DecisionNoOpinion,
|
||||
expectedEvalErr: "[cel evaluation error: expression 'request.user == 'bob'' resulted in error: no such key: user, cel evaluation error: expression 'has(request.nonResourceAttributes) && request.nonResourceAttributes.verb == 'get'' resulted in error: no such key: verb]",
|
||||
expressions: []apiserver.WebhookMatchCondition{
|
||||
{
|
||||
Expression: "request.user == 'bob'",
|
||||
},
|
||||
{
|
||||
Expression: "has(request.nonResourceAttributes) && request.nonResourceAttributes.verb == 'get'",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
wh, err := newV1Authorizer(s.URL, clientCert, clientKey, caCert, 0, noopAuthorizerMetrics(), test.expressions)
|
||||
if len(test.expectedCompileErr) > 0 && err == nil {
|
||||
t.Fatalf("%d: Expected compile error", i)
|
||||
} else if len(test.expectedCompileErr) == 0 && err != nil {
|
||||
t.Fatalf("%d: unexpected error when creating a new WebhookAuthorizer: %v", i, err)
|
||||
}
|
||||
if err != nil {
|
||||
if d := cmp.Diff(test.expectedCompileErr, err.Error()); d != "" {
|
||||
t.Fatalf("newV1Authorizer mismatch (-want +got):\n%s", d)
|
||||
}
|
||||
}
|
||||
if err == nil {
|
||||
authorized, _, err := wh.Authorize(context.Background(), test.attr)
|
||||
if len(test.expectedEvalErr) > 0 && err == nil {
|
||||
t.Fatalf("%d: Expected eval error", i)
|
||||
} else if len(test.expectedEvalErr) == 0 && err != nil {
|
||||
t.Fatalf("%d: unexpected error when authorizing: %v", i, err)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
if d := cmp.Diff(test.expectedEvalErr, err.Error()); d != "" {
|
||||
t.Fatalf("Authorize mismatch (-want +got):\n%s", d)
|
||||
}
|
||||
}
|
||||
|
||||
if test.expectedDecision != authorized {
|
||||
t.Errorf("%d: expected authorized=%v, got %v", i, test.expectedDecision, authorized)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func noopAuthorizerMetrics() AuthorizerMetrics {
|
||||
return AuthorizerMetrics{
|
||||
RecordRequestTotal: noopMetrics{}.RecordRequestTotal,
|
||||
|
@ -37,6 +37,7 @@ import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
authorizationv1beta1 "k8s.io/api/authorization/v1beta1"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
authzconfig "k8s.io/apiserver/pkg/apis/apiserver"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/apiserver/pkg/authorization/authorizer"
|
||||
webhookutil "k8s.io/apiserver/pkg/util/webhook"
|
||||
@ -195,7 +196,7 @@ current-context: default
|
||||
if err != nil {
|
||||
return fmt.Errorf("error building sar client: %v", err)
|
||||
}
|
||||
_, err = newWithBackoff(sarClient, 0, 0, testRetryBackoff, noopAuthorizerMetrics())
|
||||
_, err = newWithBackoff(sarClient, 0, 0, testRetryBackoff, []authzconfig.WebhookMatchCondition{}, noopAuthorizerMetrics())
|
||||
return err
|
||||
}()
|
||||
if err != nil && !tt.wantErr {
|
||||
@ -338,7 +339,7 @@ func newV1beta1Authorizer(callbackURL string, clientCert, clientKey, ca []byte,
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("error building sar client: %v", err)
|
||||
}
|
||||
return newWithBackoff(sarClient, cacheTime, cacheTime, testRetryBackoff, noopAuthorizerMetrics())
|
||||
return newWithBackoff(sarClient, cacheTime, cacheTime, testRetryBackoff, []authzconfig.WebhookMatchCondition{}, noopAuthorizerMetrics())
|
||||
}
|
||||
|
||||
func TestV1beta1TLSConfig(t *testing.T) {
|
||||
|
1
vendor/modules.txt
vendored
1
vendor/modules.txt
vendored
@ -1515,6 +1515,7 @@ k8s.io/apiserver/pkg/authentication/token/union
|
||||
k8s.io/apiserver/pkg/authentication/user
|
||||
k8s.io/apiserver/pkg/authorization/authorizer
|
||||
k8s.io/apiserver/pkg/authorization/authorizerfactory
|
||||
k8s.io/apiserver/pkg/authorization/cel
|
||||
k8s.io/apiserver/pkg/authorization/path
|
||||
k8s.io/apiserver/pkg/authorization/union
|
||||
k8s.io/apiserver/pkg/cel
|
||||
|
Loading…
Reference in New Issue
Block a user