Skip to content

Commit fb9c94b

Browse files
authored
Merge pull request kubernetes#121705 from liggitt/authz-config-webhook-test
Add multi-webhook integration test
2 parents 0674135 + 0112d91 commit fb9c94b

File tree

10 files changed

+404
-33
lines changed

10 files changed

+404
-33
lines changed

pkg/kubeapiserver/authorizer/config.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,11 +118,21 @@ func (config Config) New() (authorizer.Authorizer, authorizer.RuleResolver, erro
118118
if err != nil {
119119
return nil, nil, err
120120
}
121+
var decisionOnError authorizer.Decision
122+
switch configuredAuthorizer.Webhook.FailurePolicy {
123+
case authzconfig.FailurePolicyNoOpinion:
124+
decisionOnError = authorizer.DecisionNoOpinion
125+
case authzconfig.FailurePolicyDeny:
126+
decisionOnError = authorizer.DecisionDeny
127+
default:
128+
return nil, nil, fmt.Errorf("unknown failurePolicy %q", configuredAuthorizer.Webhook.FailurePolicy)
129+
}
121130
webhookAuthorizer, err := webhook.New(clientConfig,
122131
configuredAuthorizer.Webhook.SubjectAccessReviewVersion,
123132
configuredAuthorizer.Webhook.AuthorizedTTL.Duration,
124133
configuredAuthorizer.Webhook.UnauthorizedTTL.Duration,
125134
*config.WebhookRetryBackoff,
135+
decisionOnError,
126136
configuredAuthorizer.Webhook.MatchConditions,
127137
)
128138
if err != nil {

staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -512,7 +512,9 @@ func ValidateWebhookConfiguration(fldPath *field.Path, c *api.WebhookConfigurati
512512

513513
switch c.MatchConditionSubjectAccessReviewVersion {
514514
case "":
515-
allErrs = append(allErrs, field.Required(fldPath.Child("matchConditionSubjectAccessReviewVersion"), ""))
515+
if len(c.MatchConditions) > 0 {
516+
allErrs = append(allErrs, field.Required(fldPath.Child("matchConditionSubjectAccessReviewVersion"), "required if match conditions are specified"))
517+
}
516518
case "v1":
517519
_ = &v1.SubjectAccessReview{}
518520
default:

staging/src/k8s.io/apiserver/pkg/apis/apiserver/validation/validation_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1438,6 +1438,7 @@ func TestValidateAuthorizationConfiguration(t *testing.T) {
14381438
ConnectionInfo: api.WebhookConnectionInfo{
14391439
Type: "InClusterConfig",
14401440
},
1441+
MatchConditions: []api.WebhookMatchCondition{{Expression: "true"}},
14411442
},
14421443
},
14431444
},

staging/src/k8s.io/apiserver/pkg/authorization/authorizerfactory/delegating.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ func (c DelegatingAuthorizerConfig) New() (authorizer.Authorizer, error) {
5454
c.AllowCacheTTL,
5555
c.DenyCacheTTL,
5656
*c.WebhookRetryBackoff,
57+
authorizer.DecisionNoOpinion,
5758
webhook.AuthorizerMetrics{
5859
RecordRequestTotal: RecordRequestTotal,
5960
RecordRequestLatency: RecordRequestLatency,

staging/src/k8s.io/apiserver/pkg/authorization/cel/compile.go

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"github.com/google/cel-go/cel"
2323
"github.com/google/cel-go/common/types/ref"
2424

25+
authorizationv1 "k8s.io/api/authorization/v1"
2526
"k8s.io/apimachinery/pkg/util/version"
2627
apiservercel "k8s.io/apiserver/pkg/cel"
2728
"k8s.io/apiserver/pkg/cel/environment"
@@ -143,6 +144,7 @@ func mustBuildEnv(baseEnv *environment.EnvSet) *environment.EnvSet {
143144
}
144145

145146
// buildRequestType generates a DeclType for SubjectAccessReviewSpec.
147+
// if attributes are added here, also add to convertObjectToUnstructured.
146148
func buildRequestType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType {
147149
resourceAttributesType := buildResourceAttributesType(field, fields)
148150
nonResourceAttributesType := buildNonResourceAttributesType(field, fields)
@@ -157,6 +159,7 @@ func buildRequestType(field func(name string, declType *apiservercel.DeclType, r
157159
}
158160

159161
// buildResourceAttributesType generates a DeclType for ResourceAttributes.
162+
// if attributes are added here, also add to convertObjectToUnstructured.
160163
func buildResourceAttributesType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType {
161164
return apiservercel.NewObjectType("kubernetes.ResourceAttributes", fields(
162165
field("namespace", apiservercel.StringType, false),
@@ -170,9 +173,42 @@ func buildResourceAttributesType(field func(name string, declType *apiservercel.
170173
}
171174

172175
// buildNonResourceAttributesType generates a DeclType for NonResourceAttributes.
176+
// if attributes are added here, also add to convertObjectToUnstructured.
173177
func buildNonResourceAttributesType(field func(name string, declType *apiservercel.DeclType, required bool) *apiservercel.DeclField, fields func(fields ...*apiservercel.DeclField) map[string]*apiservercel.DeclField) *apiservercel.DeclType {
174178
return apiservercel.NewObjectType("kubernetes.NonResourceAttributes", fields(
175179
field("path", apiservercel.StringType, false),
176180
field("verb", apiservercel.StringType, false),
177181
))
178182
}
183+
184+
func convertObjectToUnstructured(obj *authorizationv1.SubjectAccessReviewSpec) map[string]interface{} {
185+
// Construct version containing every SubjectAccessReview user and string attribute field, even omitempty ones, for evaluation by CEL
186+
extra := obj.Extra
187+
if extra == nil {
188+
extra = map[string]authorizationv1.ExtraValue{}
189+
}
190+
ret := map[string]interface{}{
191+
"user": obj.User,
192+
"groups": obj.Groups,
193+
"uid": string(obj.UID),
194+
"extra": extra,
195+
}
196+
if obj.ResourceAttributes != nil {
197+
ret["resourceAttributes"] = map[string]string{
198+
"namespace": obj.ResourceAttributes.Namespace,
199+
"verb": obj.ResourceAttributes.Verb,
200+
"group": obj.ResourceAttributes.Group,
201+
"version": obj.ResourceAttributes.Version,
202+
"resource": obj.ResourceAttributes.Resource,
203+
"subresource": obj.ResourceAttributes.Subresource,
204+
"name": obj.ResourceAttributes.Name,
205+
}
206+
}
207+
if obj.NonResourceAttributes != nil {
208+
ret["nonResourceAttributes"] = map[string]string{
209+
"verb": obj.NonResourceAttributes.Verb,
210+
"path": obj.NonResourceAttributes.Path,
211+
}
212+
}
213+
return ret
214+
}

staging/src/k8s.io/apiserver/pkg/authorization/cel/matcher.go

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import (
2121
"fmt"
2222

2323
celgo "github.com/google/cel-go/cel"
24+
2425
authorizationv1 "k8s.io/api/authorization/v1"
25-
"k8s.io/apimachinery/pkg/runtime"
2626
utilerrors "k8s.io/apimachinery/pkg/util/errors"
2727
)
2828

@@ -33,12 +33,8 @@ type CELMatcher struct {
3333
// eval evaluates the given SubjectAccessReview against all cel matchCondition expression
3434
func (c *CELMatcher) Eval(ctx context.Context, r *authorizationv1.SubjectAccessReview) (bool, error) {
3535
var evalErrors []error
36-
specValObject, err := convertObjectToUnstructured(&r.Spec)
37-
if err != nil {
38-
return false, fmt.Errorf("authz celMatcher eval error: convert SubjectAccessReviewSpec object to unstructured failed: %w", err)
39-
}
4036
va := map[string]interface{}{
41-
"request": specValObject,
37+
"request": convertObjectToUnstructured(&r.Spec),
4238
}
4339
for _, compilationResult := range c.CompilationResults {
4440
evalResult, _, err := compilationResult.Program.ContextEval(ctx, va)
@@ -68,14 +64,3 @@ func (c *CELMatcher) Eval(ctx context.Context, r *authorizationv1.SubjectAccessR
6864
// return ALL matchConditions evaluate to TRUE successfully without error
6965
return true, nil
7066
}
71-
72-
func convertObjectToUnstructured(obj *authorizationv1.SubjectAccessReviewSpec) (map[string]interface{}, error) {
73-
if obj == nil {
74-
return nil, nil
75-
}
76-
ret, err := runtime.DefaultUnstructuredConverter.ToUnstructured(obj)
77-
if err != nil {
78-
return nil, err
79-
}
80-
return ret, nil
81-
}

staging/src/k8s.io/apiserver/plugin/pkg/authorizer/webhook/webhook.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -75,8 +75,8 @@ type WebhookAuthorizer struct {
7575
}
7676

7777
// NewFromInterface creates a WebhookAuthorizer using the given subjectAccessReview client
78-
func NewFromInterface(subjectAccessReview authorizationv1client.AuthorizationV1Interface, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, metrics AuthorizerMetrics) (*WebhookAuthorizer, error) {
79-
return newWithBackoff(&subjectAccessReviewV1Client{subjectAccessReview.RESTClient()}, authorizedTTL, unauthorizedTTL, retryBackoff, nil, metrics)
78+
func NewFromInterface(subjectAccessReview authorizationv1client.AuthorizationV1Interface, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, decisionOnError authorizer.Decision, metrics AuthorizerMetrics) (*WebhookAuthorizer, error) {
79+
return newWithBackoff(&subjectAccessReviewV1Client{subjectAccessReview.RESTClient()}, authorizedTTL, unauthorizedTTL, retryBackoff, decisionOnError, nil, metrics)
8080
}
8181

8282
// New creates a new WebhookAuthorizer from the provided kubeconfig file.
@@ -98,19 +98,19 @@ func NewFromInterface(subjectAccessReview authorizationv1client.AuthorizationV1I
9898
//
9999
// For additional HTTP configuration, refer to the kubeconfig documentation
100100
// https://kubernetes.io/docs/user-guide/kubeconfig-file/.
101-
func New(config *rest.Config, version string, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, matchConditions []apiserver.WebhookMatchCondition) (*WebhookAuthorizer, error) {
101+
func New(config *rest.Config, version string, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, decisionOnError authorizer.Decision, matchConditions []apiserver.WebhookMatchCondition) (*WebhookAuthorizer, error) {
102102
subjectAccessReview, err := subjectAccessReviewInterfaceFromConfig(config, version, retryBackoff)
103103
if err != nil {
104104
return nil, err
105105
}
106-
return newWithBackoff(subjectAccessReview, authorizedTTL, unauthorizedTTL, retryBackoff, matchConditions, AuthorizerMetrics{
106+
return newWithBackoff(subjectAccessReview, authorizedTTL, unauthorizedTTL, retryBackoff, decisionOnError, matchConditions, AuthorizerMetrics{
107107
RecordRequestTotal: noopMetrics{}.RecordRequestTotal,
108108
RecordRequestLatency: noopMetrics{}.RecordRequestLatency,
109109
})
110110
}
111111

112112
// newWithBackoff allows tests to skip the sleep.
113-
func newWithBackoff(subjectAccessReview subjectAccessReviewer, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, matchConditions []apiserver.WebhookMatchCondition, metrics AuthorizerMetrics) (*WebhookAuthorizer, error) {
113+
func newWithBackoff(subjectAccessReview subjectAccessReviewer, authorizedTTL, unauthorizedTTL time.Duration, retryBackoff wait.Backoff, decisionOnError authorizer.Decision, matchConditions []apiserver.WebhookMatchCondition, metrics AuthorizerMetrics) (*WebhookAuthorizer, error) {
114114
// compile all expressions once in validation and save the results to be used for eval later
115115
cm, fieldErr := apiservervalidation.ValidateAndCompileMatchConditions(matchConditions)
116116
if err := fieldErr.ToAggregate(); err != nil {
@@ -122,7 +122,7 @@ func newWithBackoff(subjectAccessReview subjectAccessReviewer, authorizedTTL, un
122122
authorizedTTL: authorizedTTL,
123123
unauthorizedTTL: unauthorizedTTL,
124124
retryBackoff: retryBackoff,
125-
decisionOnError: authorizer.DecisionNoOpinion,
125+
decisionOnError: decisionOnError,
126126
metrics: metrics,
127127
celMatcher: cm,
128128
}, nil

staging/src/k8s.io/apiserver/plugin/pkg/authorizer/webhook/webhook_v1_test.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
utiltesting "k8s.io/client-go/util/testing"
3838

3939
"github.com/google/go-cmp/cmp"
40+
4041
authorizationv1 "k8s.io/api/authorization/v1"
4142
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
4243
"k8s.io/apimachinery/pkg/util/wait"
@@ -209,7 +210,7 @@ current-context: default
209210
if err != nil {
210211
return fmt.Errorf("error building sar client: %v", err)
211212
}
212-
_, err = newWithBackoff(sarClient, 0, 0, testRetryBackoff, []apiserver.WebhookMatchCondition{}, noopAuthorizerMetrics())
213+
_, err = newWithBackoff(sarClient, 0, 0, testRetryBackoff, authorizer.DecisionNoOpinion, []apiserver.WebhookMatchCondition{}, noopAuthorizerMetrics())
213214
return err
214215
}()
215216
if err != nil && !tt.wantErr {
@@ -352,7 +353,7 @@ func newV1Authorizer(callbackURL string, clientCert, clientKey, ca []byte, cache
352353
if err != nil {
353354
return nil, fmt.Errorf("error building sar client: %v", err)
354355
}
355-
return newWithBackoff(sarClient, cacheTime, cacheTime, testRetryBackoff, expressions, metrics)
356+
return newWithBackoff(sarClient, cacheTime, cacheTime, testRetryBackoff, authorizer.DecisionNoOpinion, expressions, metrics)
356357
}
357358

358359
func TestV1TLSConfig(t *testing.T) {
@@ -703,6 +704,7 @@ func TestStructuredAuthzConfigFeatureEnablement(t *testing.T) {
703704
Name: "alice",
704705
UID: "1",
705706
Groups: []string{"group1", "group2"},
707+
Extra: map[string][]string{"key1": {"a", "b", "c"}},
706708
},
707709
ResourceRequest: true,
708710
Namespace: "kittensandponies",
@@ -797,6 +799,7 @@ func TestV1WebhookMatchConditions(t *testing.T) {
797799
Name: "alice",
798800
UID: "1",
799801
Groups: []string{"group1", "group2"},
802+
Extra: map[string][]string{"key1": {"a", "b", "c"}},
800803
},
801804
ResourceRequest: true,
802805
Namespace: "kittensandponies",
@@ -847,6 +850,18 @@ func TestV1WebhookMatchConditions(t *testing.T) {
847850
{
848851
Expression: "('group1' in request.groups)",
849852
},
853+
{
854+
Expression: "('key1' in request.extra)",
855+
},
856+
{
857+
Expression: "!('key2' in request.extra)",
858+
},
859+
{
860+
Expression: "('a' in request.extra['key1'])",
861+
},
862+
{
863+
Expression: "!('z' in request.extra['key1'])",
864+
},
850865
{
851866
Expression: "has(request.resourceAttributes) && request.resourceAttributes.namespace == 'kittensandponies'",
852867
},
@@ -1027,13 +1042,10 @@ func TestV1WebhookMatchConditions(t *testing.T) {
10271042
expectedCompileErr: "",
10281043
// default decisionOnError in newWithBackoff to skip
10291044
expectedDecision: authorizer.DecisionNoOpinion,
1030-
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]",
1045+
expectedEvalErr: "cel evaluation error: expression 'request.resourceAttributes.verb == 'get'' resulted in error: no such key: resourceAttributes",
10311046
expressions: []apiserver.WebhookMatchCondition{
10321047
{
1033-
Expression: "request.user == 'bob'",
1034-
},
1035-
{
1036-
Expression: "has(request.nonResourceAttributes) && request.nonResourceAttributes.verb == 'get'",
1048+
Expression: "request.resourceAttributes.verb == 'get'",
10371049
},
10381050
},
10391051
},

staging/src/k8s.io/apiserver/plugin/pkg/authorizer/webhook/webhook_v1beta1_test.go

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import (
3535
"time"
3636

3737
"github.com/google/go-cmp/cmp"
38+
3839
authorizationv1beta1 "k8s.io/api/authorization/v1beta1"
3940
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
4041
authzconfig "k8s.io/apiserver/pkg/apis/apiserver"
@@ -196,7 +197,7 @@ current-context: default
196197
if err != nil {
197198
return fmt.Errorf("error building sar client: %v", err)
198199
}
199-
_, err = newWithBackoff(sarClient, 0, 0, testRetryBackoff, []authzconfig.WebhookMatchCondition{}, noopAuthorizerMetrics())
200+
_, err = newWithBackoff(sarClient, 0, 0, testRetryBackoff, authorizer.DecisionNoOpinion, []authzconfig.WebhookMatchCondition{}, noopAuthorizerMetrics())
200201
return err
201202
}()
202203
if err != nil && !tt.wantErr {
@@ -339,7 +340,7 @@ func newV1beta1Authorizer(callbackURL string, clientCert, clientKey, ca []byte,
339340
if err != nil {
340341
return nil, fmt.Errorf("error building sar client: %v", err)
341342
}
342-
return newWithBackoff(sarClient, cacheTime, cacheTime, testRetryBackoff, []authzconfig.WebhookMatchCondition{}, noopAuthorizerMetrics())
343+
return newWithBackoff(sarClient, cacheTime, cacheTime, testRetryBackoff, authorizer.DecisionNoOpinion, []authzconfig.WebhookMatchCondition{}, noopAuthorizerMetrics())
343344
}
344345

345346
func TestV1beta1TLSConfig(t *testing.T) {

0 commit comments

Comments
 (0)