From c704d70d49bb9799674d84aecb2a49afb6512e11 Mon Sep 17 00:00:00 2001 From: Mike Danese Date: Tue, 16 Oct 2018 20:57:37 -0700 Subject: [PATCH] create audience unaware authenticator wrappers --- .../pkg/authentication/authenticator/BUILD | 10 +- .../authenticator/audagnostic.go | 90 +++++++ .../authenticator/audagnostic_test.go | 229 ++++++++++++++++++ 3 files changed, 328 insertions(+), 1 deletion(-) create mode 100644 staging/src/k8s.io/apiserver/pkg/authentication/authenticator/audagnostic.go create mode 100644 staging/src/k8s.io/apiserver/pkg/authentication/authenticator/audagnostic_test.go diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/authenticator/BUILD b/staging/src/k8s.io/apiserver/pkg/authentication/authenticator/BUILD index 22ffab2cda7..a8a5395dc92 100644 --- a/staging/src/k8s.io/apiserver/pkg/authentication/authenticator/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/authentication/authenticator/BUILD @@ -5,6 +5,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") go_library( name = "go_default_library", srcs = [ + "audagnostic.go", "audiences.go", "interfaces.go", ], @@ -28,6 +29,13 @@ filegroup( go_test( name = "go_default_test", - srcs = ["audiences_test.go"], + srcs = [ + "audagnostic_test.go", + "audiences_test.go", + ], embed = [":go_default_library"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/util/diff:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/authentication/user:go_default_library", + ], ) diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/authenticator/audagnostic.go b/staging/src/k8s.io/apiserver/pkg/authentication/authenticator/audagnostic.go new file mode 100644 index 00000000000..bcf7eb4bc9d --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/authentication/authenticator/audagnostic.go @@ -0,0 +1,90 @@ +/* +Copyright 2018 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 authenticator + +import ( + "context" + "fmt" + "net/http" +) + +func authenticate(ctx context.Context, implicitAuds Audiences, authenticate func() (*Response, bool, error)) (*Response, bool, error) { + targetAuds, ok := AudiencesFrom(ctx) + // We can remove this once api audiences is never empty. That will probably + // be N releases after TokenRequest is GA. + if !ok { + return authenticate() + } + auds := implicitAuds.Intersect(targetAuds) + if len(auds) == 0 { + return nil, false, nil + } + resp, ok, err := authenticate() + if err != nil || !ok { + return nil, false, err + } + if len(resp.Audiences) > 0 { + // maybe the authenticator was audience aware after all. + return nil, false, fmt.Errorf("audience agnostic authenticator wrapped an authenticator that returned audiences: %q", resp.Audiences) + } + resp.Audiences = auds + return resp, true, nil +} + +type audAgnosticRequestAuthenticator struct { + implicit Audiences + delegate Request +} + +var _ = Request(&audAgnosticRequestAuthenticator{}) + +func (a *audAgnosticRequestAuthenticator) AuthenticateRequest(req *http.Request) (*Response, bool, error) { + return authenticate(req.Context(), a.implicit, func() (*Response, bool, error) { + return a.delegate.AuthenticateRequest(req) + }) +} + +// WrapAudienceAgnosticRequest wraps an audience agnostic request authenticator +// to restrict its accepted audiences to a set of implicit audiences. +func WrapAudienceAgnosticRequest(implicit Audiences, delegate Request) Request { + return &audAgnosticRequestAuthenticator{ + implicit: implicit, + delegate: delegate, + } +} + +type audAgnosticTokenAuthenticator struct { + implicit Audiences + delegate Token +} + +var _ = Token(&audAgnosticTokenAuthenticator{}) + +func (a *audAgnosticTokenAuthenticator) AuthenticateToken(ctx context.Context, tok string) (*Response, bool, error) { + return authenticate(ctx, a.implicit, func() (*Response, bool, error) { + return a.delegate.AuthenticateToken(ctx, tok) + }) +} + +// WrapAudienceAgnosticToken wraps an audience agnostic token authenticator to +// restrict its accepted audiences to a set of implicit audiences. +func WrapAudienceAgnosticToken(implicit Audiences, delegate Token) Token { + return &audAgnosticTokenAuthenticator{ + implicit: implicit, + delegate: delegate, + } +} diff --git a/staging/src/k8s.io/apiserver/pkg/authentication/authenticator/audagnostic_test.go b/staging/src/k8s.io/apiserver/pkg/authentication/authenticator/audagnostic_test.go new file mode 100644 index 00000000000..0152b9a6e38 --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/authentication/authenticator/audagnostic_test.go @@ -0,0 +1,229 @@ +/* +Copyright 2018 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 authenticator + +import ( + "context" + "errors" + "fmt" + "reflect" + "testing" + + "k8s.io/apimachinery/pkg/util/diff" + "k8s.io/apiserver/pkg/authentication/user" +) + +func TestAuthenticate(t *testing.T) { + type treq struct { + resp *Response + authenticated bool + err error + + wantResp *Response + wantAuthenticated bool + wantErr bool + } + type taudcfg struct { + auds Audiences + implicitAuds Audiences + } + cs := []struct { + name string + + taudcfgs []taudcfg + treqs []treq + }{ + { + name: "good audience", + + taudcfgs: []taudcfg{ + { + implicitAuds: Audiences{"api"}, + auds: Audiences{"api"}, + }, + { + implicitAuds: Audiences{"api", "other"}, + auds: Audiences{"api"}, + }, + { + implicitAuds: Audiences{"api"}, + auds: Audiences{"api", "other"}, + }, + { + implicitAuds: Audiences{"api", "other"}, + auds: Audiences{"api", "other_other"}, + }, + }, + + treqs: []treq{ + { + resp: &Response{ + User: &user.DefaultInfo{ + Name: "test_user", + }, + }, + authenticated: true, + + wantResp: &Response{ + User: &user.DefaultInfo{ + Name: "test_user", + }, + Audiences: Audiences{"api"}, + }, + wantAuthenticated: true, + }, + { + err: errors.New("uhoh"), + wantErr: true, + }, + { + authenticated: false, + wantAuthenticated: false, + }, + }, + }, + { + name: "multiple good audiences", + + taudcfgs: []taudcfg{ + { + implicitAuds: Audiences{"api", "other_api"}, + auds: Audiences{"api", "other_api"}, + }, + { + implicitAuds: Audiences{"api", "other_api", "other"}, + auds: Audiences{"api", "other_api"}, + }, + { + implicitAuds: Audiences{"api", "other_api"}, + auds: Audiences{"api", "other_api", "other"}, + }, + { + implicitAuds: Audiences{"api", "other_api", "other"}, + auds: Audiences{"api", "other_api", "other_other"}, + }, + }, + + treqs: []treq{ + { + resp: &Response{ + User: &user.DefaultInfo{ + Name: "test_user", + }, + }, + authenticated: true, + + wantResp: &Response{ + User: &user.DefaultInfo{ + Name: "test_user", + }, + Audiences: Audiences{"api", "other_api"}, + }, + wantAuthenticated: true, + }, + { + err: errors.New("uhoh"), + + wantErr: true, + }, + { + authenticated: false, + + wantAuthenticated: false, + }, + }, + }, + { + name: "bad audience(s)", + + taudcfgs: []taudcfg{ + { + implicitAuds: Audiences{"api"}, + auds: Audiences{"other_api"}, + }, + { + implicitAuds: Audiences{}, + auds: Audiences{"other_api"}, + }, + { + implicitAuds: Audiences{"api"}, + auds: Audiences{}, + }, + { + implicitAuds: Audiences{"api", "other"}, + auds: Audiences{}, + }, + { + implicitAuds: Audiences{}, + auds: Audiences{"api", "other"}, + }, + }, + + treqs: []treq{ + { + resp: &Response{ + User: &user.DefaultInfo{ + Name: "test_user", + }, + }, + authenticated: true, + + wantAuthenticated: false, + }, + { + err: errors.New("uhoh"), + + wantAuthenticated: false, + }, + { + authenticated: false, + + wantAuthenticated: false, + }, + }, + }, + } + + for _, c := range cs { + t.Run(c.name, func(t *testing.T) { + for _, taudcfg := range c.taudcfgs { + for _, treq := range c.treqs { + t.Run(fmt.Sprintf("auds=%q,implicit=%q", taudcfg.auds, taudcfg.implicitAuds), func(t *testing.T) { + ctx := context.Background() + ctx = WithAudiences(ctx, taudcfg.auds) + resp, ok, err := authenticate(ctx, taudcfg.implicitAuds, func() (*Response, bool, error) { + if treq.resp != nil { + resp := *treq.resp + return &resp, treq.authenticated, treq.err + } + return nil, treq.authenticated, treq.err + }) + if got, want := (err != nil), treq.wantErr; got != want { + t.Errorf("Unexpected error. got=%v, want=%v, err=%v", got, want, err) + } + if got, want := ok, treq.wantAuthenticated; got != want { + t.Errorf("Unexpected authentication. got=%v, want=%v", got, want) + } + if got, want := resp, treq.wantResp; !reflect.DeepEqual(got, want) { + t.Errorf("Unexpected response. diff:\n%v", diff.ObjectGoPrintDiff(got, want)) + } + }) + } + } + }) + } +}