diff --git a/staging/src/BUILD b/staging/src/BUILD index c7ad9ec90e9..a57c071a261 100644 --- a/staging/src/BUILD +++ b/staging/src/BUILD @@ -89,6 +89,7 @@ filegroup( "//staging/src/k8s.io/apiserver/pkg/authentication/user:all-srcs", "//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:all-srcs", "//staging/src/k8s.io/apiserver/pkg/authorization/authorizerfactory:all-srcs", + "//staging/src/k8s.io/apiserver/pkg/authorization/path:all-srcs", "//staging/src/k8s.io/apiserver/pkg/authorization/union:all-srcs", "//staging/src/k8s.io/apiserver/pkg/endpoints:all-srcs", "//staging/src/k8s.io/apiserver/pkg/features:all-srcs", diff --git a/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json b/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json index 859adaa3c36..8d45965fc2c 100644 --- a/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json +++ b/staging/src/k8s.io/apiextensions-apiserver/Godeps/Godeps.json @@ -1334,6 +1334,10 @@ "ImportPath": "k8s.io/apiserver/pkg/authorization/authorizerfactory", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/apiserver/pkg/authorization/path", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/apiserver/pkg/authorization/union", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" diff --git a/staging/src/k8s.io/apiserver/pkg/authorization/authorizerfactory/delegating.go b/staging/src/k8s.io/apiserver/pkg/authorization/authorizerfactory/delegating.go index 25b5aa9899d..c75c0a7552b 100644 --- a/staging/src/k8s.io/apiserver/pkg/authorization/authorizerfactory/delegating.go +++ b/staging/src/k8s.io/apiserver/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/staging/src/k8s.io/apiserver/pkg/authorization/path/BUILD b/staging/src/k8s.io/apiserver/pkg/authorization/path/BUILD new file mode 100644 index 00000000000..4bbbe48ed7f --- /dev/null +++ b/staging/src/k8s.io/apiserver/pkg/authorization/path/BUILD @@ -0,0 +1,37 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "go_default_library", + srcs = [ + "doc.go", + "path.go", + ], + importmap = "k8s.io/kubernetes/vendor/k8s.io/apiserver/pkg/authorization/path", + importpath = "k8s.io/apiserver/pkg/authorization/path", + visibility = ["//visibility:public"], + deps = [ + "//staging/src/k8s.io/apimachinery/pkg/util/sets:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library", + ], +) + +filegroup( + name = "package-srcs", + srcs = glob(["**"]), + tags = ["automanaged"], + visibility = ["//visibility:private"], +) + +filegroup( + name = "all-srcs", + srcs = [":package-srcs"], + tags = ["automanaged"], + visibility = ["//visibility:public"], +) + +go_test( + name = "go_default_test", + srcs = ["path_test.go"], + embed = [":go_default_library"], + deps = ["//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library"], +) diff --git a/staging/src/k8s.io/apiserver/pkg/authorization/path/doc.go b/staging/src/k8s.io/apiserver/pkg/authorization/path/doc.go new file mode 100644 index 00000000000..743d945b46b --- /dev/null +++ b/staging/src/k8s.io/apiserver/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/staging/src/k8s.io/apiserver/pkg/authorization/path/path.go b/staging/src/k8s.io/apiserver/pkg/authorization/path/path.go new file mode 100644 index 00000000000..03f524b38cf --- /dev/null +++ b/staging/src/k8s.io/apiserver/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/staging/src/k8s.io/apiserver/pkg/authorization/path/path_test.go b/staging/src/k8s.io/apiserver/pkg/authorization/path/path_test.go new file mode 100644 index 00000000000..be48c52bc88 --- /dev/null +++ b/staging/src/k8s.io/apiserver/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/staging/src/k8s.io/apiserver/pkg/server/options/BUILD b/staging/src/k8s.io/apiserver/pkg/server/options/BUILD index 1e3df709a5a..f646cf66f19 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/BUILD +++ b/staging/src/k8s.io/apiserver/pkg/server/options/BUILD @@ -44,7 +44,10 @@ go_library( "//staging/src/k8s.io/apiserver/pkg/audit:go_default_library", "//staging/src/k8s.io/apiserver/pkg/audit/policy:go_default_library", "//staging/src/k8s.io/apiserver/pkg/authentication/authenticatorfactory:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/authorization/authorizer:go_default_library", "//staging/src/k8s.io/apiserver/pkg/authorization/authorizerfactory:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/authorization/path:go_default_library", + "//staging/src/k8s.io/apiserver/pkg/authorization/union:go_default_library", "//staging/src/k8s.io/apiserver/pkg/features:go_default_library", "//staging/src/k8s.io/apiserver/pkg/registry/generic:go_default_library", "//staging/src/k8s.io/apiserver/pkg/registry/generic/registry:go_default_library", diff --git a/staging/src/k8s.io/apiserver/pkg/server/options/authorization.go b/staging/src/k8s.io/apiserver/pkg/server/options/authorization.go index eadf3d1e1c5..738faa1d446 100644 --- a/staging/src/k8s.io/apiserver/pkg/server/options/authorization.go +++ b/staging/src/k8s.io/apiserver/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() + a, err := s.ToAuthorization() if err != nil { return err } - authorizer, err := cfg.New() - if err != nil { - return err - } - - c.Authorizer = authorizer + c.Authorizer = a return nil } -func (s *DelegatingAuthorizationOptions) ToAuthorizationConfig() (authorizerfactory.DelegatingAuthorizerConfig, error) { - sarClient, err := s.newSubjectAccessReview() - if err != nil { - return authorizerfactory.DelegatingAuthorizerConfig{}, err +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) } - ret := authorizerfactory.DelegatingAuthorizerConfig{ + sarClient, err := s.newSubjectAccessReview() + if err != nil { + return nil, err + } + 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) { diff --git a/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json b/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json index b7a6228b44d..316aff47486 100644 --- a/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json +++ b/staging/src/k8s.io/kube-aggregator/Godeps/Godeps.json @@ -1018,6 +1018,10 @@ "ImportPath": "k8s.io/apiserver/pkg/authorization/authorizerfactory", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/apiserver/pkg/authorization/path", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/apiserver/pkg/authorization/union", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" diff --git a/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json b/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json index f278a085649..ddad64ab35e 100644 --- a/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json +++ b/staging/src/k8s.io/sample-apiserver/Godeps/Godeps.json @@ -990,6 +990,10 @@ "ImportPath": "k8s.io/apiserver/pkg/authorization/authorizerfactory", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" }, + { + "ImportPath": "k8s.io/apiserver/pkg/authorization/path", + "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" + }, { "ImportPath": "k8s.io/apiserver/pkg/authorization/union", "Rev": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"