From 496ae45d6fb8e48c0104e615139331fa95057965 Mon Sep 17 00:00:00 2001 From: "Dr. Stefan Schimanski" Date: Fri, 17 Aug 2018 17:03:16 +0200 Subject: [PATCH] delegated authz: add AlwaysAllowPaths mechanism to exclude e.g. /healthz Kubernetes-commit: 6142e2f8f7c8b1c5d32a2f9aa3715ea0b5baf167 --- .../authorizerfactory/delegating.go | 3 +- pkg/authorization/path/doc.go | 18 +++++ pkg/authorization/path/path.go | 67 ++++++++++++++++ pkg/authorization/path/path_test.go | 77 +++++++++++++++++++ pkg/server/options/authorization.go | 49 ++++++++---- 5 files changed, 198 insertions(+), 16 deletions(-) create mode 100644 pkg/authorization/path/doc.go create mode 100644 pkg/authorization/path/path.go create mode 100644 pkg/authorization/path/path_test.go diff --git a/pkg/authorization/authorizerfactory/delegating.go b/pkg/authorization/authorizerfactory/delegating.go index 25b5aa989..c75c0a755 100644 --- a/pkg/authorization/authorizerfactory/delegating.go +++ b/pkg/authorization/authorizerfactory/delegating.go @@ -20,9 +20,8 @@ import ( "time" "k8s.io/apiserver/pkg/authorization/authorizer" - authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1beta1" - "k8s.io/apiserver/plugin/pkg/authorizer/webhook" + authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1beta1" ) // DelegatingAuthorizerConfig is the minimal configuration needed to create an authenticator diff --git a/pkg/authorization/path/doc.go b/pkg/authorization/path/doc.go new file mode 100644 index 000000000..743d945b4 --- /dev/null +++ b/pkg/authorization/path/doc.go @@ -0,0 +1,18 @@ +/* +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 path contains an authorizer that allows certain paths and path prefixes. +package path diff --git a/pkg/authorization/path/path.go b/pkg/authorization/path/path.go new file mode 100644 index 000000000..03f524b38 --- /dev/null +++ b/pkg/authorization/path/path.go @@ -0,0 +1,67 @@ +/* +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 path + +import ( + "fmt" + "strings" + + "k8s.io/apimachinery/pkg/util/sets" + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +// NewAuthorizer returns an authorizer which accepts a given set of paths. +// Each path is either a fully matching path or it ends in * in case a prefix match is done. A leading / is optional. +func NewAuthorizer(alwaysAllowPaths []string) (authorizer.Authorizer, error) { + var prefixes []string + paths := sets.NewString() + for _, p := range alwaysAllowPaths { + p = strings.TrimPrefix(p, "/") + if len(p) == 0 { + // matches "/" + paths.Insert(p) + continue + } + if strings.ContainsRune(p[:len(p)-1], '*') { + return nil, fmt.Errorf("only trailing * allowed in %q", p) + } + if strings.HasSuffix(p, "*") { + prefixes = append(prefixes, p[:len(p)-1]) + } else { + paths.Insert(p) + } + } + + return authorizer.AuthorizerFunc(func(a authorizer.Attributes) (authorizer.Decision, string, error) { + if a.IsResourceRequest() { + return authorizer.DecisionNoOpinion, "", nil + } + + pth := strings.TrimPrefix(a.GetPath(), "/") + if paths.Has(pth) { + return authorizer.DecisionAllow, "", nil + } + + for _, prefix := range prefixes { + if strings.HasPrefix(pth, prefix) { + return authorizer.DecisionAllow, "", nil + } + } + + return authorizer.DecisionNoOpinion, "", nil + }), nil +} diff --git a/pkg/authorization/path/path_test.go b/pkg/authorization/path/path_test.go new file mode 100644 index 000000000..be48c52bc --- /dev/null +++ b/pkg/authorization/path/path_test.go @@ -0,0 +1,77 @@ +/* +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 path + +import ( + "testing" + + "k8s.io/apiserver/pkg/authorization/authorizer" +) + +func TestNewAuthorizer(t *testing.T) { + tests := []struct { + name string + excludedPaths []string + allowed, denied, noOpinion []string + wantErr bool + }{ + {"inner star", []string{"/foo*bar"}, nil, nil, nil, true}, + {"double star", []string{"/foo**"}, nil, nil, nil, true}, + {"empty", nil, nil, nil, []string{"/"}, false}, + {"slash", []string{"/"}, []string{"/"}, nil, []string{"/foo", "//"}, false}, + {"foo", []string{"/foo"}, []string{"/foo", "foo"}, nil, []string{"/", "", "/bar", "/foo/", "/fooooo", "//foo"}, false}, + {"foo slash", []string{"/foo/"}, []string{"/foo/"}, nil, []string{"/", "", "/bar", "/foo", "/fooooo"}, false}, + {"foo slash star", []string{"/foo/*"}, []string{"/foo/", "/foo/bar/bla"}, nil, []string{"/", "", "/foo", "/bar", "/fooooo"}, false}, + {"foo bar", []string{"/foo", "/bar"}, []string{"/foo", "/bar"}, nil, []string{"/", "", "/foo/", "/bar/", "/fooooo"}, false}, + {"foo star", []string{"/foo*"}, []string{"/foo", "/foooo"}, nil, []string{"/", "", "/fo", "/bar"}, false}, + {"star", []string{"/*"}, []string{"/", "", "/foo", "/foooo"}, nil, nil, false}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + a, err := NewAuthorizer(tt.excludedPaths) + if err != nil && !tt.wantErr { + t.Fatalf("unexpected error: %v", err) + } + if err == nil && tt.wantErr { + t.Fatalf("expected error, didn't get any") + } + if err != nil { + return + } + + for _, cases := range []struct { + paths []string + want authorizer.Decision + }{ + {tt.allowed, authorizer.DecisionAllow}, + {tt.denied, authorizer.DecisionDeny}, + {tt.noOpinion, authorizer.DecisionNoOpinion}, + } { + for _, pth := range cases.paths { + info := authorizer.AttributesRecord{ + Path: pth, + } + if got, _, err := a.Authorize(info); err != nil { + t.Errorf("NewAuthorizer(%v).Authorize(%q) return unexpected error: %v", tt.excludedPaths, pth, err) + } else if got != cases.want { + t.Errorf("NewAuthorizer(%v).Authorize(%q) = %v, want %v", tt.excludedPaths, pth, got, cases.want) + } + } + } + }) + } +} diff --git a/pkg/server/options/authorization.go b/pkg/server/options/authorization.go index eadf3d1e1..738faa1d4 100644 --- a/pkg/server/options/authorization.go +++ b/pkg/server/options/authorization.go @@ -22,7 +22,10 @@ import ( "github.com/spf13/pflag" + "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/authorization/authorizerfactory" + "k8s.io/apiserver/pkg/authorization/path" + "k8s.io/apiserver/pkg/authorization/union" "k8s.io/apiserver/pkg/server" authorizationclient "k8s.io/client-go/kubernetes/typed/authorization/v1beta1" "k8s.io/client-go/rest" @@ -45,6 +48,10 @@ type DelegatingAuthorizationOptions struct { // DenyCacheTTL is the length of time that an unsuccessful authorization response will be cached. // You generally want more responsive, "deny, try again" flows. DenyCacheTTL time.Duration + + // AlwaysAllowPaths are HTTP paths which are excluded from authorization. They can be plain + // paths or end in * in which case prefix-match is applied. A leading / is optional. + AlwaysAllowPaths []string } func NewDelegatingAuthorizationOptions() *DelegatingAuthorizationOptions { @@ -65,9 +72,9 @@ func (s *DelegatingAuthorizationOptions) AddFlags(fs *pflag.FlagSet) { return } - fs.StringVar(&s.RemoteKubeConfigFile, "authorization-kubeconfig", s.RemoteKubeConfigFile, ""+ + fs.StringVar(&s.RemoteKubeConfigFile, "authorization-kubeconfig", s.RemoteKubeConfigFile, "kubeconfig file pointing at the 'core' kubernetes server with enough rights to create "+ - " subjectaccessreviews.authorization.k8s.io.") + " subjectaccessreviews.authorization.k8s.io.") fs.DurationVar(&s.AllowCacheTTL, "authorization-webhook-cache-authorized-ttl", s.AllowCacheTTL, @@ -76,6 +83,10 @@ func (s *DelegatingAuthorizationOptions) AddFlags(fs *pflag.FlagSet) { fs.DurationVar(&s.DenyCacheTTL, "authorization-webhook-cache-unauthorized-ttl", s.DenyCacheTTL, "The duration to cache 'unauthorized' responses from the webhook authorizer.") + + fs.StringSliceVar(&s.AlwaysAllowPaths, "authorization-always-allow-paths", s.AlwaysAllowPaths, + "A list of HTTP paths to skip during authorization, i.e. these are authorized without "+ + "contacting the 'core' kubernetes server.") } func (s *DelegatingAuthorizationOptions) ApplyTo(c *server.AuthorizationInfo) error { @@ -84,31 +95,41 @@ func (s *DelegatingAuthorizationOptions) ApplyTo(c *server.AuthorizationInfo) er return nil } - cfg, err := s.ToAuthorizationConfig() - if err != nil { - return err - } - authorizer, err := cfg.New() + a, err := s.ToAuthorization() if err != nil { return err } - - c.Authorizer = authorizer + c.Authorizer = a return nil } -func (s *DelegatingAuthorizationOptions) ToAuthorizationConfig() (authorizerfactory.DelegatingAuthorizerConfig, error) { +func (s *DelegatingAuthorizationOptions) ToAuthorization() (authorizer.Authorizer, error) { + var authorizers []authorizer.Authorizer + + if len(s.AlwaysAllowPaths) > 0 { + a, err := path.NewAuthorizer(s.AlwaysAllowPaths) + if err != nil { + return nil, err + } + authorizers = append(authorizers, a) + } + sarClient, err := s.newSubjectAccessReview() if err != nil { - return authorizerfactory.DelegatingAuthorizerConfig{}, err + return nil, err } - - ret := authorizerfactory.DelegatingAuthorizerConfig{ + cfg := authorizerfactory.DelegatingAuthorizerConfig{ SubjectAccessReviewClient: sarClient, AllowCacheTTL: s.AllowCacheTTL, DenyCacheTTL: s.DenyCacheTTL, } - return ret, nil + a, err := cfg.New() + if err != nil { + return nil, err + } + authorizers = append(authorizers, a) + + return union.New(authorizers...), nil } func (s *DelegatingAuthorizationOptions) newSubjectAccessReview() (authorizationclient.SubjectAccessReviewInterface, error) {