implement authorizer filter
Signed-off-by: hongming <talonwan@yunify.com>
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
190
pkg/apiserver/authorization/authorizer/interfaces.go
Normal file
190
pkg/apiserver/authorization/authorizer/interfaces.go
Normal file
@@ -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
|
||||
)
|
||||
73
pkg/apiserver/authorization/authorizer/rule.go
Normal file
73
pkg/apiserver/authorization/authorizer/rule.go
Normal file
@@ -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
|
||||
}
|
||||
32
pkg/apiserver/authorization/authorizerfactory/opa.go
Normal file
32
pkg/apiserver/authorization/authorizerfactory/opa.go
Normal file
@@ -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)
|
||||
}
|
||||
18
pkg/apiserver/authorization/path/doc.go
Normal file
18
pkg/apiserver/authorization/path/doc.go
Normal file
@@ -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"
|
||||
67
pkg/apiserver/authorization/path/path.go
Normal file
67
pkg/apiserver/authorization/path/path.go
Normal file
@@ -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
|
||||
}
|
||||
77
pkg/apiserver/authorization/path/path_test.go
Normal file
77
pkg/apiserver/authorization/path/path_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
105
pkg/apiserver/authorization/union/union.go
Normal file
105
pkg/apiserver/authorization/union/union.go
Normal file
@@ -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)
|
||||
}
|
||||
265
pkg/apiserver/authorization/union/union_test.go
Normal file
265
pkg/apiserver/authorization/union/union_test.go
Normal file
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
94
pkg/apiserver/request/requestinfo_test.go
Normal file
94
pkg/apiserver/request/requestinfo_test.go
Normal file
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user