diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 23209166c..91f8ed3fe 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -9,13 +9,15 @@ import ( urlruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/apimachinery/pkg/util/sets" "k8s.io/apiserver/pkg/authentication/request/bearertoken" - "k8s.io/apiserver/pkg/authentication/request/union" - "k8s.io/apiserver/pkg/authorization/authorizerfactory" + unionauth "k8s.io/apiserver/pkg/authentication/request/union" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/klog" "kubesphere.io/kubesphere/pkg/api/iam" "kubesphere.io/kubesphere/pkg/apiserver/authentication/authenticators/jwttoken" authenticationrequest "kubesphere.io/kubesphere/pkg/apiserver/authentication/request" + "kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizerfactory" + "kubesphere.io/kubesphere/pkg/apiserver/authorization/path" + unionauthorizer "kubesphere.io/kubesphere/pkg/apiserver/authorization/union" "kubesphere.io/kubesphere/pkg/apiserver/dispatch" "kubesphere.io/kubesphere/pkg/apiserver/filters" "kubesphere.io/kubesphere/pkg/apiserver/request" @@ -179,13 +181,17 @@ func (s *APIServer) buildHandlerChain() { }) handler := s.Server.Handler - handler = filters.WithKubeAPIServer(handler, s.KubernetesClient.Config(), &errorResponder{}) - handler = filters.WithMultipleClusterDispatcher(handler, dispatch.DefaultClusterDispatch) - handler = filters.WithAuthorization(handler, authorizerfactory.NewAlwaysAllowAuthorizer()) - authn := union.New(&authenticationrequest.AnonymousAuthenticator{}, bearertoken.New(jwttoken.NewTokenAuthenticator(s.CacheClient, s.AuthenticateOptions.JwtSecret))) - handler = filters.WithAuthentication(handler, authn, failed) handler = filters.WithRequestInfo(handler, requestInfoResolver) + authn := unionauth.New(&authenticationrequest.AnonymousAuthenticator{}, bearertoken.New(jwttoken.NewTokenAuthenticator(s.CacheClient, s.AuthenticateOptions.JwtSecret))) + handler = filters.WithAuthentication(handler, authn, failed) + + excludedPaths := []string{"/oauth/authorize", "/oauth/token"} + pathAuthorizer, _ := path.NewAuthorizer(excludedPaths) + authorizer := unionauthorizer.New(pathAuthorizer, authorizerfactory.NewOPAAuthorizer()) + handler = filters.WithAuthorization(handler, authorizer) + handler = filters.WithMultipleClusterDispatcher(handler, dispatch.DefaultClusterDispatch) + handler = filters.WithKubeAPIServer(handler, s.KubernetesClient.Config(), &errorResponder{}) s.Server.Handler = handler } @@ -194,7 +200,7 @@ func (s *APIServer) waitForResourceSync(stopCh <-chan struct{}) error { klog.V(0).Info("Start cache objects") discoveryClient := s.KubernetesClient.Kubernetes().Discovery() - apiResourcesList, err := discoveryClient.ServerResources() + _, apiResourcesList, err := discoveryClient.ServerGroupsAndResources() if err != nil { return err } diff --git a/pkg/apiserver/authorization/authorizer/interfaces.go b/pkg/apiserver/authorization/authorizer/interfaces.go new file mode 100644 index 000000000..261c320b2 --- /dev/null +++ b/pkg/apiserver/authorization/authorizer/interfaces.go @@ -0,0 +1,190 @@ +/* +Copyright 2014 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 authorizer + +import ( + "net/http" + + "k8s.io/apiserver/pkg/authentication/user" +) + +// Attributes is an interface used by an Authorizer to get information about a request +// that is used to make an authorization decision. +type Attributes interface { + // GetUser returns the user.Info object to authorize + GetUser() user.Info + + // GetVerb returns the kube verb associated with API requests (this includes get, list, watch, create, update, patch, delete, deletecollection, and proxy), + // or the lowercased HTTP verb associated with non-API requests (this includes get, put, post, patch, and delete) + GetVerb() string + + // When IsReadOnly() == true, the request has no side effects, other than + // caching, logging, and other incidentals. + IsReadOnly() bool + + // Indicates whether or not the request should be handled by kubernetes or kubesphere + IsKubernetesRequest() bool + + // The cluster of the object, if a request is for a REST object. + GetCluster() string + + // The workspace of the object, if a request is for a REST object. + GetWorkspace() string + + // The namespace of the object, if a request is for a REST object. + GetNamespace() string + + // The devops project of the object, if a request is for a REST object. + GetDevopsProject() string + + // The kind of object, if a request is for a REST object. + GetResource() string + + // GetSubresource returns the subresource being requested, if present + GetSubresource() string + + // GetName returns the name of the object as parsed off the request. This will not be present for all request types, but + // will be present for: get, update, delete + GetName() string + + // The group of the resource, if a request is for a REST object. + GetAPIGroup() string + + // GetAPIVersion returns the version of the group requested, if a request is for a REST object. + GetAPIVersion() string + + // IsResourceRequest returns true for requests to API resources, like /api/v1/nodes, + // and false for non-resource endpoints like /api, /healthz + IsResourceRequest() bool + + // GetPath returns the path of the request + GetPath() string +} + +// Authorizer makes an authorization decision based on information gained by making +// zero or more calls to methods of the Attributes interface. It returns nil when an action is +// authorized, otherwise it returns an error. +type Authorizer interface { + Authorize(a Attributes) (authorized Decision, reason string, err error) +} + +type AuthorizerFunc func(a Attributes) (Decision, string, error) + +func (f AuthorizerFunc) Authorize(a Attributes) (Decision, string, error) { + return f(a) +} + +// RuleResolver provides a mechanism for resolving the list of rules that apply to a given user within a namespace. +type RuleResolver interface { + // RulesFor get the list of cluster wide rules, the list of rules in the specific namespace, incomplete status and errors. + RulesFor(user user.Info, namespace string) ([]ResourceRuleInfo, []NonResourceRuleInfo, bool, error) +} + +// RequestAttributesGetter provides a function that extracts Attributes from an http.Request +type RequestAttributesGetter interface { + GetRequestAttributes(user.Info, *http.Request) Attributes +} + +// AttributesRecord implements Attributes interface. +type AttributesRecord struct { + User user.Info + Verb string + Cluster string + Workspace string + Namespace string + DevopsProject string + APIGroup string + APIVersion string + Resource string + Subresource string + Name string + KubernetesRequest bool + ResourceRequest bool + Path string +} + +func (a AttributesRecord) GetUser() user.Info { + return a.User +} + +func (a AttributesRecord) GetVerb() string { + return a.Verb +} + +func (a AttributesRecord) IsReadOnly() bool { + return a.Verb == "get" || a.Verb == "list" || a.Verb == "watch" +} + +func (a AttributesRecord) GetCluster() string { + return a.Cluster +} + +func (a AttributesRecord) GetWorkspace() string { + return a.Workspace +} + +func (a AttributesRecord) GetNamespace() string { + return a.Namespace +} + +func (a AttributesRecord) GetDevopsProject() string { + return a.DevopsProject +} + +func (a AttributesRecord) GetResource() string { + return a.Resource +} + +func (a AttributesRecord) GetSubresource() string { + return a.Subresource +} + +func (a AttributesRecord) GetName() string { + return a.Name +} + +func (a AttributesRecord) GetAPIGroup() string { + return a.APIGroup +} + +func (a AttributesRecord) GetAPIVersion() string { + return a.APIVersion +} + +func (a AttributesRecord) IsResourceRequest() bool { + return a.ResourceRequest +} + +func (a AttributesRecord) IsKubernetesRequest() bool { + return a.KubernetesRequest +} + +func (a AttributesRecord) GetPath() string { + return a.Path +} + +type Decision int + +const ( + // DecisionDeny means that an authorizer decided to deny the action. + DecisionDeny Decision = iota + // DecisionAllow means that an authorizer decided to allow the action. + DecisionAllow + // DecisionNoOpionion means that an authorizer has no opinion on whether + // to allow or deny an action. + DecisionNoOpinion +) diff --git a/pkg/apiserver/authorization/authorizer/rule.go b/pkg/apiserver/authorization/authorizer/rule.go new file mode 100644 index 000000000..8f7d9d9ef --- /dev/null +++ b/pkg/apiserver/authorization/authorizer/rule.go @@ -0,0 +1,73 @@ +/* +Copyright 2017 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 authorizer + +type ResourceRuleInfo interface { + // GetVerbs returns a list of kubernetes resource API verbs. + GetVerbs() []string + // GetAPIGroups return the names of the APIGroup that contains the resources. + GetAPIGroups() []string + // GetResources return a list of resources the rule applies to. + GetResources() []string + // GetResourceNames return a white list of names that the rule applies to. + GetResourceNames() []string +} + +// DefaultResourceRuleInfo holds information that describes a rule for the resource +type DefaultResourceRuleInfo struct { + Verbs []string + APIGroups []string + Resources []string + ResourceNames []string +} + +func (i *DefaultResourceRuleInfo) GetVerbs() []string { + return i.Verbs +} + +func (i *DefaultResourceRuleInfo) GetAPIGroups() []string { + return i.APIGroups +} + +func (i *DefaultResourceRuleInfo) GetResources() []string { + return i.Resources +} + +func (i *DefaultResourceRuleInfo) GetResourceNames() []string { + return i.ResourceNames +} + +type NonResourceRuleInfo interface { + // GetVerbs returns a list of kubernetes resource API verbs. + GetVerbs() []string + // GetNonResourceURLs return a set of partial urls that a user should have access to. + GetNonResourceURLs() []string +} + +// DefaultNonResourceRuleInfo holds information that describes a rule for the non-resource +type DefaultNonResourceRuleInfo struct { + Verbs []string + NonResourceURLs []string +} + +func (i *DefaultNonResourceRuleInfo) GetVerbs() []string { + return i.Verbs +} + +func (i *DefaultNonResourceRuleInfo) GetNonResourceURLs() []string { + return i.NonResourceURLs +} diff --git a/pkg/apiserver/authorization/authorizerfactory/opa.go b/pkg/apiserver/authorization/authorizerfactory/opa.go new file mode 100644 index 000000000..06c2aba84 --- /dev/null +++ b/pkg/apiserver/authorization/authorizerfactory/opa.go @@ -0,0 +1,32 @@ +/* + * + * Copyright 2020 The KubeSphere 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 authorizerfactory + +import "kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer" + +type opaAuthorizer struct{} + +func (opaAuthorizer) Authorize(a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) { + // TODO implement. + return authorizer.DecisionAllow, "", nil +} + +func NewOPAAuthorizer() *opaAuthorizer { + return new(opaAuthorizer) +} diff --git a/pkg/apiserver/authorization/path/doc.go b/pkg/apiserver/authorization/path/doc.go new file mode 100644 index 000000000..654aaeb74 --- /dev/null +++ b/pkg/apiserver/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 // import "k8s.io/apiserver/pkg/authorization/path" diff --git a/pkg/apiserver/authorization/path/path.go b/pkg/apiserver/authorization/path/path.go new file mode 100644 index 000000000..4df9c41a5 --- /dev/null +++ b/pkg/apiserver/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" + "kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer" + "strings" + + "k8s.io/apimachinery/pkg/util/sets" +) + +// 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/apiserver/authorization/path/path_test.go b/pkg/apiserver/authorization/path/path_test.go new file mode 100644 index 000000000..be48c52bc --- /dev/null +++ b/pkg/apiserver/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/apiserver/authorization/union/union.go b/pkg/apiserver/authorization/union/union.go new file mode 100644 index 000000000..1e3dfac97 --- /dev/null +++ b/pkg/apiserver/authorization/union/union.go @@ -0,0 +1,105 @@ +/* +Copyright 2014 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 union implements an authorizer that combines multiple subauthorizer. +// The union authorizer iterates over each subauthorizer and returns the first +// decision that is either an Allow decision or a Deny decision. If a +// subauthorizer returns a NoOpinion, then the union authorizer moves onto the +// next authorizer or, if the subauthorizer was the last authorizer, returns +// NoOpinion as the aggregate decision. I.e. union authorizer creates an +// aggregate decision and supports short-circuit allows and denies from +// subauthorizers. +package union + +import ( + "kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer" + "strings" + + utilerrors "k8s.io/apimachinery/pkg/util/errors" + "k8s.io/apiserver/pkg/authentication/user" +) + +// unionAuthzHandler authorizer against a chain of authorizer.Authorizer +type unionAuthzHandler []authorizer.Authorizer + +// New returns an authorizer that authorizes against a chain of authorizer.Authorizer objects +func New(authorizationHandlers ...authorizer.Authorizer) authorizer.Authorizer { + return unionAuthzHandler(authorizationHandlers) +} + +// Authorizes against a chain of authorizer.Authorizer objects and returns nil if successful and returns error if unsuccessful +func (authzHandler unionAuthzHandler) Authorize(a authorizer.Attributes) (authorizer.Decision, string, error) { + var ( + errlist []error + reasonlist []string + ) + + for _, currAuthzHandler := range authzHandler { + decision, reason, err := currAuthzHandler.Authorize(a) + + if err != nil { + errlist = append(errlist, err) + } + if len(reason) != 0 { + reasonlist = append(reasonlist, reason) + } + switch decision { + case authorizer.DecisionAllow, authorizer.DecisionDeny: + return decision, reason, err + case authorizer.DecisionNoOpinion: + // continue to the next authorizer + } + } + + return authorizer.DecisionNoOpinion, strings.Join(reasonlist, "\n"), utilerrors.NewAggregate(errlist) +} + +// unionAuthzRulesHandler authorizer against a chain of authorizer.RuleResolver +type unionAuthzRulesHandler []authorizer.RuleResolver + +// NewRuleResolvers returns an authorizer that authorizes against a chain of authorizer.Authorizer objects +func NewRuleResolvers(authorizationHandlers ...authorizer.RuleResolver) authorizer.RuleResolver { + return unionAuthzRulesHandler(authorizationHandlers) +} + +// RulesFor against a chain of authorizer.RuleResolver objects and returns nil if successful and returns error if unsuccessful +func (authzHandler unionAuthzRulesHandler) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) { + var ( + errList []error + resourceRulesList []authorizer.ResourceRuleInfo + nonResourceRulesList []authorizer.NonResourceRuleInfo + ) + incompleteStatus := false + + for _, currAuthzHandler := range authzHandler { + resourceRules, nonResourceRules, incomplete, err := currAuthzHandler.RulesFor(user, namespace) + + if incomplete == true { + incompleteStatus = true + } + if err != nil { + errList = append(errList, err) + } + if len(resourceRules) > 0 { + resourceRulesList = append(resourceRulesList, resourceRules...) + } + if len(nonResourceRules) > 0 { + nonResourceRulesList = append(nonResourceRulesList, nonResourceRules...) + } + } + + return resourceRulesList, nonResourceRulesList, incompleteStatus, utilerrors.NewAggregate(errList) +} diff --git a/pkg/apiserver/authorization/union/union_test.go b/pkg/apiserver/authorization/union/union_test.go new file mode 100644 index 000000000..592629eeb --- /dev/null +++ b/pkg/apiserver/authorization/union/union_test.go @@ -0,0 +1,265 @@ +/* +Copyright 2014 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 union + +import ( + "errors" + "fmt" + "k8s.io/apiserver/pkg/authentication/user" + "kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer" + "reflect" + "testing" +) + +type mockAuthzHandler struct { + decision authorizer.Decision + err error +} + +func (mock *mockAuthzHandler) Authorize(a authorizer.Attributes) (authorizer.Decision, string, error) { + return mock.decision, "", mock.err +} + +func TestAuthorizationSecondPasses(t *testing.T) { + handler1 := &mockAuthzHandler{decision: authorizer.DecisionNoOpinion} + handler2 := &mockAuthzHandler{decision: authorizer.DecisionAllow} + authzHandler := New(handler1, handler2) + + authorized, _, _ := authzHandler.Authorize(nil) + if authorized != authorizer.DecisionAllow { + t.Errorf("Unexpected authorization failure") + } +} + +func TestAuthorizationFirstPasses(t *testing.T) { + handler1 := &mockAuthzHandler{decision: authorizer.DecisionAllow} + handler2 := &mockAuthzHandler{decision: authorizer.DecisionNoOpinion} + authzHandler := New(handler1, handler2) + + authorized, _, _ := authzHandler.Authorize(nil) + if authorized != authorizer.DecisionAllow { + t.Errorf("Unexpected authorization failure") + } +} + +func TestAuthorizationNonePasses(t *testing.T) { + handler1 := &mockAuthzHandler{decision: authorizer.DecisionNoOpinion} + handler2 := &mockAuthzHandler{decision: authorizer.DecisionNoOpinion} + authzHandler := New(handler1, handler2) + + authorized, _, _ := authzHandler.Authorize(nil) + if authorized == authorizer.DecisionAllow { + t.Errorf("Expected failed authorization") + } +} + +func TestAuthorizationError(t *testing.T) { + handler1 := &mockAuthzHandler{err: fmt.Errorf("foo")} + handler2 := &mockAuthzHandler{err: fmt.Errorf("foo")} + authzHandler := New(handler1, handler2) + + _, _, err := authzHandler.Authorize(nil) + if err == nil { + t.Errorf("Expected error: %v", err) + } +} + +type mockAuthzRuleHandler struct { + resourceRules []authorizer.ResourceRuleInfo + nonResourceRules []authorizer.NonResourceRuleInfo + err error +} + +func (mock *mockAuthzRuleHandler) RulesFor(user user.Info, namespace string) ([]authorizer.ResourceRuleInfo, []authorizer.NonResourceRuleInfo, bool, error) { + if mock.err != nil { + return []authorizer.ResourceRuleInfo{}, []authorizer.NonResourceRuleInfo{}, false, mock.err + } + return mock.resourceRules, mock.nonResourceRules, false, nil +} + +func TestAuthorizationResourceRules(t *testing.T) { + handler1 := &mockAuthzRuleHandler{ + resourceRules: []authorizer.ResourceRuleInfo{ + &authorizer.DefaultResourceRuleInfo{ + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"bindings"}, + }, + &authorizer.DefaultResourceRuleInfo{ + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + }, + }, + } + handler2 := &mockAuthzRuleHandler{ + resourceRules: []authorizer.ResourceRuleInfo{ + &authorizer.DefaultResourceRuleInfo{ + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"events"}, + }, + &authorizer.DefaultResourceRuleInfo{ + Verbs: []string{"get"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + ResourceNames: []string{"foo"}, + }, + }, + } + + expected := []authorizer.DefaultResourceRuleInfo{ + { + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"bindings"}, + }, + { + Verbs: []string{"get", "list", "watch"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + }, + { + Verbs: []string{"*"}, + APIGroups: []string{"*"}, + Resources: []string{"events"}, + }, + { + Verbs: []string{"get"}, + APIGroups: []string{"*"}, + Resources: []string{"*"}, + ResourceNames: []string{"foo"}, + }, + } + + authzRulesHandler := NewRuleResolvers(handler1, handler2) + + rules, _, _, _ := authzRulesHandler.RulesFor(nil, "") + actual := getResourceRules(rules) + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected: \n%#v\n but actual: \n%#v\n", expected, actual) + } +} + +func TestAuthorizationNonResourceRules(t *testing.T) { + handler1 := &mockAuthzRuleHandler{ + nonResourceRules: []authorizer.NonResourceRuleInfo{ + &authorizer.DefaultNonResourceRuleInfo{ + Verbs: []string{"get"}, + NonResourceURLs: []string{"/api"}, + }, + }, + } + + handler2 := &mockAuthzRuleHandler{ + nonResourceRules: []authorizer.NonResourceRuleInfo{ + &authorizer.DefaultNonResourceRuleInfo{ + Verbs: []string{"get"}, + NonResourceURLs: []string{"/api/*"}, + }, + }, + } + + expected := []authorizer.DefaultNonResourceRuleInfo{ + { + Verbs: []string{"get"}, + NonResourceURLs: []string{"/api"}, + }, + { + Verbs: []string{"get"}, + NonResourceURLs: []string{"/api/*"}, + }, + } + + authzRulesHandler := NewRuleResolvers(handler1, handler2) + + _, rules, _, _ := authzRulesHandler.RulesFor(nil, "") + actual := getNonResourceRules(rules) + if !reflect.DeepEqual(expected, actual) { + t.Errorf("Expected: \n%#v\n but actual: \n%#v\n", expected, actual) + } +} + +func getResourceRules(infos []authorizer.ResourceRuleInfo) []authorizer.DefaultResourceRuleInfo { + rules := make([]authorizer.DefaultResourceRuleInfo, len(infos)) + for i, info := range infos { + rules[i] = authorizer.DefaultResourceRuleInfo{ + Verbs: info.GetVerbs(), + APIGroups: info.GetAPIGroups(), + Resources: info.GetResources(), + ResourceNames: info.GetResourceNames(), + } + } + return rules +} + +func getNonResourceRules(infos []authorizer.NonResourceRuleInfo) []authorizer.DefaultNonResourceRuleInfo { + rules := make([]authorizer.DefaultNonResourceRuleInfo, len(infos)) + for i, info := range infos { + rules[i] = authorizer.DefaultNonResourceRuleInfo{ + Verbs: info.GetVerbs(), + NonResourceURLs: info.GetNonResourceURLs(), + } + } + return rules +} + +func TestAuthorizationUnequivocalDeny(t *testing.T) { + cs := []struct { + authorizers []authorizer.Authorizer + decision authorizer.Decision + }{ + { + authorizers: []authorizer.Authorizer{}, + decision: authorizer.DecisionNoOpinion, + }, + { + authorizers: []authorizer.Authorizer{ + &mockAuthzHandler{decision: authorizer.DecisionNoOpinion}, + &mockAuthzHandler{decision: authorizer.DecisionAllow}, + &mockAuthzHandler{decision: authorizer.DecisionDeny}, + }, + decision: authorizer.DecisionAllow, + }, + { + authorizers: []authorizer.Authorizer{ + &mockAuthzHandler{decision: authorizer.DecisionNoOpinion}, + &mockAuthzHandler{decision: authorizer.DecisionDeny}, + &mockAuthzHandler{decision: authorizer.DecisionAllow}, + }, + decision: authorizer.DecisionDeny, + }, + { + authorizers: []authorizer.Authorizer{ + &mockAuthzHandler{decision: authorizer.DecisionNoOpinion}, + &mockAuthzHandler{decision: authorizer.DecisionDeny, err: errors.New("webhook failed closed")}, + &mockAuthzHandler{decision: authorizer.DecisionAllow}, + }, + decision: authorizer.DecisionDeny, + }, + } + for i, c := range cs { + t.Run(fmt.Sprintf("case %v", i), func(t *testing.T) { + authzHandler := New(c.authorizers...) + + decision, _, _ := authzHandler.Authorize(nil) + if decision != c.decision { + t.Errorf("Unexpected authorization failure: %v, expected: %v", decision, c.decision) + } + }) + } +} diff --git a/pkg/apiserver/filters/authorization.go b/pkg/apiserver/filters/authorization.go index d76d79dab..2388df5ce 100644 --- a/pkg/apiserver/filters/authorization.go +++ b/pkg/apiserver/filters/authorization.go @@ -3,10 +3,10 @@ package filters import ( "context" "errors" - "k8s.io/apiserver/pkg/authorization/authorizer" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" k8srequest "k8s.io/apiserver/pkg/endpoints/request" "k8s.io/klog" + "kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer" "kubesphere.io/kubesphere/pkg/apiserver/request" "net/http" ) @@ -59,12 +59,16 @@ func GetAuthorizerAttributes(ctx context.Context) (authorizer.Attributes, error) attribs.ResourceRequest = requestInfo.IsResourceRequest attribs.Path = requestInfo.Path attribs.Verb = requestInfo.Verb + attribs.Cluster = requestInfo.Cluster + attribs.Workspace = requestInfo.Workspace + attribs.KubernetesRequest = requestInfo.IsKubernetesRequest attribs.APIGroup = requestInfo.APIGroup attribs.APIVersion = requestInfo.APIVersion attribs.Resource = requestInfo.Resource attribs.Subresource = requestInfo.Subresource attribs.Namespace = requestInfo.Namespace + attribs.DevopsProject = requestInfo.DevopsProject attribs.Name = requestInfo.Name return &attribs, nil diff --git a/pkg/apiserver/request/requestinfo.go b/pkg/apiserver/request/requestinfo.go index 4a85ff99a..002ca32cf 100644 --- a/pkg/apiserver/request/requestinfo.go +++ b/pkg/apiserver/request/requestinfo.go @@ -26,20 +26,22 @@ var kubernetesAPIPrefixes = sets.NewString("api", "apis") type RequestInfo struct { *k8srequest.RequestInfo - // IsKubeSphereRequest indicates whether or not the request should be handled by kubernetes or kubesphere + // IsKubernetesRequest indicates whether or not the request should be handled by kubernetes or kubesphere IsKubernetesRequest bool - // Workspace of requested namespace, for non-workspaced resources, this may be empty + // Workspace of requested resource, for non-workspaced resources, this may be empty Workspace string // Cluster of requested resource, this is empty in single-cluster environment Cluster string + + // Devops project of requested resource, this may be empty + DevopsProject string } type RequestInfoFactory struct { - APIPrefixes sets.String - GrouplessAPIPrefixes sets.String - k8sRequestInfoFactory *k8srequest.RequestInfoFactory + APIPrefixes sets.String + GrouplessAPIPrefixes sets.String } // NewRequestInfo returns the information from the http request. If error is not nil, RequestInfo holds the information as best it is known before the failure diff --git a/pkg/apiserver/request/requestinfo_test.go b/pkg/apiserver/request/requestinfo_test.go new file mode 100644 index 000000000..41fda5397 --- /dev/null +++ b/pkg/apiserver/request/requestinfo_test.go @@ -0,0 +1,94 @@ +/* + * + * Copyright 2020 The KubeSphere 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 request + +import ( + "k8s.io/apimachinery/pkg/util/sets" + "net/http" + "testing" +) + +func newTestRequestInfoResolver() RequestInfoResolver { + requestInfoResolver := &RequestInfoFactory{ + APIPrefixes: sets.NewString("api", "apis", "kapis", "kapi"), + GrouplessAPIPrefixes: sets.NewString("api", "kapi"), + } + + return requestInfoResolver +} + +func TestRequestInfoFactory_NewRequestInfo(t *testing.T) { + tests := []struct { + name string + url string + method string + expectedErr error + expectedVerb string + expectedResource string + expectedIsResourceRequest bool + expectedCluster string + }{ + { + name: "login", + url: "/oauth/authorize?client_id=ks-console&response_type=token", + method: http.MethodPost, + expectedErr: nil, + expectedVerb: "POST", + expectedResource: "", + expectedIsResourceRequest: false, + expectedCluster: "", + }, + { + name: "list namespaces", + url: "/kapis/resources.kubesphere.io/v1alpha2/namespaces", + method: http.MethodGet, + expectedErr: nil, + expectedVerb: "list", + expectedResource: "namespaces", + expectedIsResourceRequest: true, + expectedCluster: "", + }, + } + + requestInfoResolver := newTestRequestInfoResolver() + + for _, test := range tests { + req, err := http.NewRequest(test.method, test.url, nil) + if err != nil { + t.Fatal(err) + } + requestInfo, err := requestInfoResolver.NewRequestInfo(req) + + if err != nil { + if test.expectedErr != err { + t.Errorf("%s: expected error %v, actual %v", test.name, test.expectedErr, err) + } + } else { + if test.expectedVerb != requestInfo.Verb { + t.Errorf("%s: expected verb %v, actual %+v", test.name, test.expectedVerb, requestInfo.Verb) + } + if test.expectedResource != requestInfo.Resource { + t.Errorf("%s: expected resource %v, actual %+v", test.name, test.expectedResource, requestInfo.Resource) + } + if test.expectedIsResourceRequest != requestInfo.IsResourceRequest { + t.Errorf("%s: expected is resource request %v, actual %+v", test.name, test.expectedIsResourceRequest, requestInfo.IsResourceRequest) + } + } + } +}