From aadb5e91e88417b692df8d6fb073f21290008bdd Mon Sep 17 00:00:00 2001 From: hongming Date: Sat, 21 Mar 2020 20:10:39 +0800 Subject: [PATCH] update Signed-off-by: hongming --- pkg/api/iam/token/jwt.go | 6 +- pkg/api/iam/token/jwt_test.go | 12 +-- pkg/api/iam/token/user.go | 5 +- pkg/api/iam/user.go | 8 +- pkg/apiserver/apiserver.go | 14 +-- .../authenticators/jwttoken/jwt_token.go | 13 +-- pkg/apiserver/filters/authentication.go | 25 ++++- pkg/apiserver/filters/authorization.go | 9 +- pkg/apiserver/filters/dispatch.go | 5 +- pkg/apiserver/filters/kubeapiserver.go | 4 + pkg/apiserver/request/context.go | 96 +++++++++++++++++++ pkg/apiserver/request/context_test.go | 93 ++++++++++++++++++ pkg/models/iam/im/im.go | 2 +- 13 files changed, 254 insertions(+), 38 deletions(-) create mode 100644 pkg/apiserver/request/context.go create mode 100644 pkg/apiserver/request/context_test.go diff --git a/pkg/api/iam/token/jwt.go b/pkg/api/iam/token/jwt.go index b85fe38d5..b9ebd7065 100644 --- a/pkg/api/iam/token/jwt.go +++ b/pkg/api/iam/token/jwt.go @@ -37,13 +37,13 @@ func (s *jwtTokenIssuer) Verify(tokenString string) (User, error) { return nil, err } - return &iam.User{Username: clm.Username, Email: clm.UID}, nil + return &iam.User{Username: clm.Username, UID: clm.UID}, nil } func (s *jwtTokenIssuer) IssueTo(user User) (string, error) { clm := &claims{ - Username: user.Name(), - UID: user.UID(), + Username: user.GetName(), + UID: user.GetUID(), StandardClaims: jwt.StandardClaims{ IssuedAt: time.Now().Unix(), Issuer: s.name, diff --git a/pkg/api/iam/token/jwt_test.go b/pkg/api/iam/token/jwt_test.go index ae5343b68..ec7741bda 100644 --- a/pkg/api/iam/token/jwt_test.go +++ b/pkg/api/iam/token/jwt_test.go @@ -12,22 +12,22 @@ func TestJwtTokenIssuer(t *testing.T) { testCases := []struct { description string name string - email string + uid string }{ { - name: "admin", - email: "admin@kubesphere.io", + name: "admin", + uid: "b8be6edd-2c92-4535-9b2a-df6326474458", }, { - name: "bar", - email: "bar@kubesphere.io", + name: "bar", + uid: "b8be6edd-2c92-4535-9b2a-df6326474452", }, } for _, testCase := range testCases { user := &iam.User{ Username: testCase.name, - Email: testCase.email, + UID: testCase.uid, } t.Run(testCase.description, func(t *testing.T) { diff --git a/pkg/api/iam/token/user.go b/pkg/api/iam/token/user.go index d29768dd8..d4a6c7f56 100644 --- a/pkg/api/iam/token/user.go +++ b/pkg/api/iam/token/user.go @@ -2,7 +2,8 @@ package token type User interface { // Name - Name() string + GetName() string - UID() string + // UID + GetUID() string } diff --git a/pkg/api/iam/user.go b/pkg/api/iam/user.go index 759ad9cfd..6ca874863 100644 --- a/pkg/api/iam/user.go +++ b/pkg/api/iam/user.go @@ -7,6 +7,7 @@ import ( type User struct { Username string `json:"username"` + UID string `json:"uid"` Email string `json:"email"` Lang string `json:"lang,omitempty"` Description string `json:"description"` @@ -18,6 +19,7 @@ type User struct { func NewUser() *User { return &User{ Username: "", + UID: "", Email: "", Lang: "", Description: "", @@ -27,12 +29,12 @@ func NewUser() *User { } } -func (u *User) Name() string { +func (u *User) GetName() string { return u.Username } -func (u *User) UID() string { - return u.Email +func (u *User) GetUID() string { + return "" } func (u *User) Validate() error { diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 335352bb4..d3f3460a2 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -175,23 +175,19 @@ func (s *APIServer) buildHandlerChain() { GrouplessAPIPrefixes: sets.NewString("api", "kapi"), } - failed := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - w.WriteHeader(http.StatusUnauthorized) - }) - handler := s.Server.Handler - 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) + handler = filters.WithKubeAPIServer(handler, s.KubernetesClient.Config(), &errorResponder{}) + handler = filters.WithMultipleClusterDispatcher(handler, dispatch.DefaultClusterDispatch) excludedPaths := []string{"/oauth/authorize", "/oauth/token"} pathAuthorizer, _ := path.NewAuthorizer(excludedPaths) authorizer := unionauthorizer.New(pathAuthorizer, authorizerfactory.NewOPAAuthorizer(am.NewFakeAMOperator(cache.NewSimpleCache()))) handler = filters.WithAuthorization(handler, authorizer) - handler = filters.WithMultipleClusterDispatcher(handler, dispatch.DefaultClusterDispatch) - handler = filters.WithKubeAPIServer(handler, s.KubernetesClient.Config(), &errorResponder{}) + authn := unionauth.New(&authenticationrequest.AnonymousAuthenticator{}, bearertoken.New(jwttoken.NewTokenAuthenticator(s.CacheClient, s.AuthenticateOptions.JwtSecret))) + handler = filters.WithAuthentication(handler, authn) + handler = filters.WithRequestInfo(handler, requestInfoResolver) s.Server.Handler = handler } diff --git a/pkg/apiserver/authentication/authenticators/jwttoken/jwt_token.go b/pkg/apiserver/authentication/authenticators/jwttoken/jwt_token.go index f860d979c..59f7d2515 100644 --- a/pkg/apiserver/authentication/authenticators/jwttoken/jwt_token.go +++ b/pkg/apiserver/authentication/authenticators/jwttoken/jwt_token.go @@ -35,17 +35,18 @@ func (t *tokenAuthenticator) AuthenticateToken(ctx context.Context, token string return nil, false, err } - _, err = t.cacheClient.Get(tokenKeyForUsername(providedUser.Name(), token)) - if err != nil { - return nil, false, errTokenExpired - } + // TODO implement token cache + //_, err = t.cacheClient.Get(tokenKeyForUsername(providedUser.Name(), token)) + //if err != nil { + // return nil, false, errTokenExpired + //} // Should we need to refresh token? return &authenticator.Response{ User: &user.DefaultInfo{ - Name: providedUser.Name(), - UID: providedUser.UID(), + Name: providedUser.GetName(), + UID: providedUser.GetUID(), Groups: []string{user.AllAuthenticated}, }, }, true, nil diff --git a/pkg/apiserver/filters/authentication.go b/pkg/apiserver/filters/authentication.go index 43d06bb3a..b4a3d92c0 100644 --- a/pkg/apiserver/filters/authentication.go +++ b/pkg/apiserver/filters/authentication.go @@ -1,27 +1,44 @@ package filters import ( + "errors" + apierrors "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/schema" + "k8s.io/apimachinery/pkg/runtime/serializer" "k8s.io/apiserver/pkg/authentication/authenticator" - "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/klog" + "kubesphere.io/kubesphere/pkg/apiserver/request" "net/http" ) // WithAuthentication installs authentication handler to handler chain. -func WithAuthentication(handler http.Handler, auth authenticator.Request, failed http.Handler) http.Handler { +func WithAuthentication(handler http.Handler, auth authenticator.Request) http.Handler { if auth == nil { klog.Warningf("Authentication is disabled") return handler } + + s := serializer.NewCodecFactory(runtime.NewScheme()).WithoutConversion() + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { - //authenticationStart := time.Now() resp, ok, err := auth.AuthenticateRequest(req) if err != nil || !ok { if err != nil { klog.Errorf("Unable to authenticate the request due to error: %v", err) } - failed.ServeHTTP(w, req) + + ctx := req.Context() + requestInfo, found := request.RequestInfoFrom(ctx) + if !found { + responsewriters.InternalError(w, req, errors.New("no RequestInfo found in the context")) + return + } + + gv := schema.GroupVersion{Group: requestInfo.APIGroup, Version: requestInfo.APIVersion} + responsewriters.ErrorNegotiated(apierrors.NewUnauthorized("Unauthorized"), s, gv, w, req) return } diff --git a/pkg/apiserver/filters/authorization.go b/pkg/apiserver/filters/authorization.go index 583a94145..fb63f97ba 100644 --- a/pkg/apiserver/filters/authorization.go +++ b/pkg/apiserver/filters/authorization.go @@ -3,8 +3,9 @@ package filters import ( "context" "errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/runtime/serializer" "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" @@ -18,6 +19,8 @@ func WithAuthorization(handler http.Handler, a authorizer.Authorizer) http.Handl return handler } + serializer := serializer.NewCodecFactory(runtime.NewScheme()).WithoutConversion() + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { ctx := req.Context() @@ -38,14 +41,14 @@ func WithAuthorization(handler http.Handler, a authorizer.Authorizer) http.Handl } klog.V(4).Infof("Forbidden: %#v, Reason: %q", req.RequestURI, reason) - w.WriteHeader(http.StatusForbidden) + responsewriters.Forbidden(ctx, attributes, w, req, reason, serializer) }) } func GetAuthorizerAttributes(ctx context.Context) (authorizer.Attributes, error) { attribs := authorizer.AttributesRecord{} - user, ok := k8srequest.UserFrom(ctx) + user, ok := request.UserFrom(ctx) if ok { attribs.User = user } diff --git a/pkg/apiserver/filters/dispatch.go b/pkg/apiserver/filters/dispatch.go index c893e6dac..1157f4b4c 100644 --- a/pkg/apiserver/filters/dispatch.go +++ b/pkg/apiserver/filters/dispatch.go @@ -7,6 +7,7 @@ import ( "kubesphere.io/kubesphere/pkg/apiserver/dispatch" "kubesphere.io/kubesphere/pkg/apiserver/request" "net/http" + "strings" ) // Multiple cluster dispatcher forward request to desired cluster based on request cluster name @@ -23,9 +24,11 @@ func WithMultipleClusterDispatcher(handler http.Handler, dispatch dispatch.Dispa return } - if info.Cluster == "" { + if info.Cluster == "host-cluster" || info.Cluster == "" { handler.ServeHTTP(w, req) } else { + // remove cluster path + req.URL.Path = strings.Replace(req.URL.Path, fmt.Sprintf("/clusters/%s", info.Cluster), "", 1) dispatch.Dispatch(w, req) } }) diff --git a/pkg/apiserver/filters/kubeapiserver.go b/pkg/apiserver/filters/kubeapiserver.go index c7eb02cfe..009ed5dc6 100644 --- a/pkg/apiserver/filters/kubeapiserver.go +++ b/pkg/apiserver/filters/kubeapiserver.go @@ -1,6 +1,7 @@ package filters import ( + "fmt" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" "k8s.io/client-go/rest" "k8s.io/klog" @@ -8,6 +9,7 @@ import ( "kubesphere.io/kubesphere/pkg/server/errors" "net/http" "net/url" + "strings" "k8s.io/apimachinery/pkg/util/proxy" ) @@ -33,6 +35,8 @@ func WithKubeAPIServer(handler http.Handler, config *rest.Config, failed proxy.E s := *req.URL s.Host = kubernetes.Host s.Scheme = kubernetes.Scheme + // remove cluster path + s.Path = strings.Replace(s.Path, fmt.Sprintf("/clusters/%s", info.Cluster), "", 1) httpProxy := proxy.NewUpgradeAwareHandler(&s, defaultTransport, true, false, failed) httpProxy.ServeHTTP(w, req) diff --git a/pkg/apiserver/request/context.go b/pkg/apiserver/request/context.go new file mode 100644 index 000000000..fe3ae38ed --- /dev/null +++ b/pkg/apiserver/request/context.go @@ -0,0 +1,96 @@ +/* +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 request + +import ( + "context" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/apis/audit" + "k8s.io/apiserver/pkg/authentication/user" +) + +// The key type is unexported to prevent collisions +type key int + +const ( + // namespaceKey is the context key for the request namespace. + namespaceKey key = iota + + // userKey is the context key for the request user. + userKey + + // auditKey is the context key for the audit event. + auditKey + + // audiencesKey is the context key for request audiences. + audiencesKey +) + +// NewContext instantiates a base context object for request flows. +func NewContext() context.Context { + return context.TODO() +} + +// NewDefaultContext instantiates a base context object for request flows in the default namespace +func NewDefaultContext() context.Context { + return WithNamespace(NewContext(), metav1.NamespaceDefault) +} + +// WithValue returns a copy of parent in which the value associated with key is val. +func WithValue(parent context.Context, key interface{}, val interface{}) context.Context { + return context.WithValue(parent, key, val) +} + +// WithNamespace returns a copy of parent in which the namespace value is set +func WithNamespace(parent context.Context, namespace string) context.Context { + return WithValue(parent, namespaceKey, namespace) +} + +// NamespaceFrom returns the value of the namespace key on the ctx +func NamespaceFrom(ctx context.Context) (string, bool) { + namespace, ok := ctx.Value(namespaceKey).(string) + return namespace, ok +} + +// NamespaceValue returns the value of the namespace key on the ctx, or the empty string if none +func NamespaceValue(ctx context.Context) string { + namespace, _ := NamespaceFrom(ctx) + return namespace +} + +// WithUser returns a copy of parent in which the user value is set +func WithUser(parent context.Context, user user.Info) context.Context { + return WithValue(parent, userKey, user) +} + +// UserFrom returns the value of the user key on the ctx +func UserFrom(ctx context.Context) (user.Info, bool) { + user, ok := ctx.Value(userKey).(user.Info) + return user, ok +} + +// WithAuditEvent returns set audit event struct. +func WithAuditEvent(parent context.Context, ev *audit.Event) context.Context { + return WithValue(parent, auditKey, ev) +} + +// AuditEventFrom returns the audit event struct on the ctx +func AuditEventFrom(ctx context.Context) *audit.Event { + ev, _ := ctx.Value(auditKey).(*audit.Event) + return ev +} diff --git a/pkg/apiserver/request/context_test.go b/pkg/apiserver/request/context_test.go new file mode 100644 index 000000000..72b3124b4 --- /dev/null +++ b/pkg/apiserver/request/context_test.go @@ -0,0 +1,93 @@ +/* +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 request + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/authentication/user" +) + +// TestNamespaceContext validates that a namespace can be get/set on a context object +func TestNamespaceContext(t *testing.T) { + ctx := NewDefaultContext() + result, ok := NamespaceFrom(ctx) + if !ok { + t.Fatalf("Error getting namespace") + } + if metav1.NamespaceDefault != result { + t.Fatalf("Expected: %s, Actual: %s", metav1.NamespaceDefault, result) + } + + ctx = NewContext() + result, ok = NamespaceFrom(ctx) + if ok { + t.Fatalf("Should not be ok because there is no namespace on the context") + } +} + +//TestUserContext validates that a userinfo can be get/set on a context object +func TestUserContext(t *testing.T) { + ctx := NewContext() + _, ok := UserFrom(ctx) + if ok { + t.Fatalf("Should not be ok because there is no user.Info on the context") + } + ctx = WithUser( + ctx, + &user.DefaultInfo{ + Name: "bob", + UID: "123", + Groups: []string{"group1"}, + Extra: map[string][]string{"foo": {"bar"}}, + }, + ) + + result, ok := UserFrom(ctx) + if !ok { + t.Fatalf("Error getting user info") + } + + expectedName := "bob" + if result.GetName() != expectedName { + t.Fatalf("Get user name error, Expected: %s, Actual: %s", expectedName, result.GetName()) + } + + expectedUID := "123" + if result.GetUID() != expectedUID { + t.Fatalf("Get UID error, Expected: %s, Actual: %s", expectedUID, result.GetName()) + } + + expectedGroup := "group1" + actualGroup := result.GetGroups() + if len(actualGroup) != 1 { + t.Fatalf("Get user group number error, Expected: 1, Actual: %d", len(actualGroup)) + } else if actualGroup[0] != expectedGroup { + t.Fatalf("Get user group error, Expected: %s, Actual: %s", expectedGroup, actualGroup[0]) + } + + expectedExtraKey := "foo" + expectedExtraValue := "bar" + actualExtra := result.GetExtra() + if len(actualExtra[expectedExtraKey]) != 1 { + t.Fatalf("Get user extra map number error, Expected: 1, Actual: %d", len(actualExtra[expectedExtraKey])) + } else if actualExtra[expectedExtraKey][0] != expectedExtraValue { + t.Fatalf("Get user extra map value error, Expected: %s, Actual: %s", expectedExtraValue, actualExtra[expectedExtraKey]) + } + +} diff --git a/pkg/models/iam/im/im.go b/pkg/models/iam/im/im.go index 063fc1913..59d3b5a9e 100644 --- a/pkg/models/iam/im/im.go +++ b/pkg/models/iam/im/im.go @@ -187,7 +187,7 @@ func (im *imOperator) VerifyToken(tokenString string) (*iam.User, error) { return nil, err } - user, err := im.ldapClient.Get(providedUser.Name()) + user, err := im.ldapClient.Get(providedUser.GetName()) if err != nil { return nil, err }