clean old devops code
todo impl use informer Signed-off-by: runzexia <runzexia@yunify.com>
This commit is contained in:
@@ -8,7 +8,6 @@ import (
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
urlruntime "k8s.io/apimachinery/pkg/util/runtime"
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apiserver/pkg/authentication/request/bearertoken"
|
||||
unionauth "k8s.io/apiserver/pkg/authentication/request/union"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
"k8s.io/klog"
|
||||
@@ -16,6 +15,7 @@ import (
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/authenticators/jwttoken"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/request/anonymous"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/request/basictoken"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/request/bearertoken"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/token"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizerfactory"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/path"
|
||||
@@ -30,7 +30,7 @@ import (
|
||||
devopsv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/devops/v1alpha2"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/iam/v1alpha2"
|
||||
loggingv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/logging/v1alpha2"
|
||||
monitoringv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/monitoring/v1alpha2"
|
||||
monitoringv1alpha3 "kubesphere.io/kubesphere/pkg/kapis/monitoring/v1alpha3"
|
||||
"kubesphere.io/kubesphere/pkg/kapis/oauth"
|
||||
openpitrixv1 "kubesphere.io/kubesphere/pkg/kapis/openpitrix/v1"
|
||||
operationsv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/operations/v1alpha2"
|
||||
@@ -137,12 +137,15 @@ func (s *APIServer) installKubeSphereAPIs() {
|
||||
urlruntime.Must(configv1alpha2.AddToContainer(s.container, s.Config))
|
||||
urlruntime.Must(resourcev1alpha3.AddToContainer(s.container, s.InformerFactory))
|
||||
urlruntime.Must(loggingv1alpha2.AddToContainer(s.container, s.KubernetesClient, s.LoggingClient))
|
||||
urlruntime.Must(monitoringv1alpha2.AddToContainer(s.container, s.KubernetesClient, s.MonitoringClient))
|
||||
urlruntime.Must(monitoringv1alpha3.AddToContainer(s.container, s.KubernetesClient.Kubernetes(), s.MonitoringClient))
|
||||
urlruntime.Must(openpitrixv1.AddToContainer(s.container, s.InformerFactory, s.OpenpitrixClient))
|
||||
urlruntime.Must(operationsv1alpha2.AddToContainer(s.container, s.KubernetesClient.Kubernetes()))
|
||||
urlruntime.Must(resourcesv1alpha2.AddToContainer(s.container, s.KubernetesClient.Kubernetes(), s.InformerFactory))
|
||||
urlruntime.Must(terminalv1alpha2.AddToContainer(s.container, s.KubernetesClient.Kubernetes(), s.KubernetesClient.Config()))
|
||||
urlruntime.Must(iamv1alpha2.AddToContainer(s.container, s.KubernetesClient, s.InformerFactory, s.LdapClient, s.CacheClient, s.Config.AuthenticationOptions))
|
||||
urlruntime.Must(iamv1alpha2.AddToContainer(s.container, im.NewOperator(s.KubernetesClient.KubeSphere(),
|
||||
s.InformerFactory.KubeSphereSharedInformerFactory()),
|
||||
am.NewAMOperator(s.KubernetesClient.KubeSphere(), s.InformerFactory.KubeSphereSharedInformerFactory()),
|
||||
s.Config.AuthenticationOptions))
|
||||
urlruntime.Must(oauth.AddToContainer(s.container, token.NewJwtTokenIssuer(token.DefaultIssuerName, s.Config.AuthenticationOptions, s.CacheClient), s.Config.AuthenticationOptions))
|
||||
urlruntime.Must(servicemeshv1alpha2.AddToContainer(s.container))
|
||||
devopsv1alpha2Service := ksruntime.NewWebService(devopsv1alpha2.GroupVersion)
|
||||
@@ -185,18 +188,20 @@ func (s *APIServer) buildHandlerChain() {
|
||||
|
||||
handler := s.Server.Handler
|
||||
handler = filters.WithKubeAPIServer(handler, s.KubernetesClient.Config(), &errorResponder{})
|
||||
handler = filters.WithMultipleClusterDispatcher(handler, dispatch.NewClusterDispatch(s.InformerFactory.KubeSphereSharedInformerFactory().Tower().V1alpha1().Agents().Lister()))
|
||||
|
||||
clusterDispatcher := dispatch.NewClusterDispatch(s.InformerFactory.KubeSphereSharedInformerFactory().Cluster().V1alpha1().Agents().Lister(), s.InformerFactory.KubeSphereSharedInformerFactory().Cluster().V1alpha1().Clusters().Lister())
|
||||
handler = filters.WithMultipleClusterDispatcher(handler, clusterDispatcher)
|
||||
|
||||
excludedPaths := []string{"/oauth/*", "/kapis/config.kubesphere.io/*"}
|
||||
pathAuthorizer, _ := path.NewAuthorizer(excludedPaths)
|
||||
|
||||
// union authorizers are ordered, don't change the order here
|
||||
authorizers := unionauthorizer.New(pathAuthorizer, authorizerfactory.NewOPAAuthorizer(am.NewFakeAMOperator()))
|
||||
authorizers := unionauthorizer.New(pathAuthorizer, authorizerfactory.NewOPAAuthorizer(am.NewAMOperator(s.KubernetesClient.KubeSphere(), s.InformerFactory.KubeSphereSharedInformerFactory())))
|
||||
handler = filters.WithAuthorization(handler, authorizers)
|
||||
|
||||
// authenticators are unordered
|
||||
authn := unionauth.New(anonymous.NewAuthenticator(),
|
||||
basictoken.New(basic.NewBasicAuthenticator(im.NewFakeOperator())),
|
||||
basictoken.New(basic.NewBasicAuthenticator(im.NewOperator(s.KubernetesClient.KubeSphere(), s.InformerFactory.KubeSphereSharedInformerFactory()))),
|
||||
bearertoken.New(jwttoken.NewTokenAuthenticator(token.NewJwtTokenIssuer(token.DefaultIssuerName, s.Config.AuthenticationOptions, s.CacheClient))))
|
||||
handler = filters.WithAuthentication(handler, authn)
|
||||
handler = filters.WithRequestInfo(handler, requestInfoResolver)
|
||||
@@ -276,6 +281,10 @@ func (s *APIServer) waitForResourceSync(stopCh <-chan struct{}) error {
|
||||
|
||||
ksGVRs := []schema.GroupVersionResource{
|
||||
{Group: "tenant.kubesphere.io", Version: "v1alpha1", Resource: "workspaces"},
|
||||
{Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "users"},
|
||||
{Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "roles"},
|
||||
{Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "rolebindings"},
|
||||
{Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "policyrules"},
|
||||
{Group: "tower.kubesphere.io", Version: "v1alpha1", Resource: "agents"},
|
||||
}
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ func (t *basicAuthenticator) AuthenticatePassword(ctx context.Context, username,
|
||||
return &authenticator.Response{
|
||||
User: &user.DefaultInfo{
|
||||
Name: providedUser.GetName(),
|
||||
UID: providedUser.GetUID(),
|
||||
UID: string(providedUser.GetUID()),
|
||||
Groups: []string{user.AllAuthenticated},
|
||||
},
|
||||
}, true, nil
|
||||
|
||||
@@ -69,4 +69,5 @@ func (options *AuthenticationOptions) AddFlags(fs *pflag.FlagSet, s *Authenticat
|
||||
fs.IntVar(&options.MaxAuthenticateRetries, "authenticate-max-retries", s.MaxAuthenticateRetries, "")
|
||||
fs.BoolVar(&options.MultipleLogin, "multiple-login", s.MultipleLogin, "Allow multiple login with the same account, disable means only one user can login at the same time.")
|
||||
fs.StringVar(&options.JwtSecret, "jwt-secret", s.JwtSecret, "Secret to sign jwt token, must not be empty.")
|
||||
fs.DurationVar(&options.OAuthOptions.AccessTokenMaxAge, "access-token-max-age", s.OAuthOptions.AccessTokenMaxAge, "AccessTokenMaxAgeSeconds control the lifetime of access tokens, 0 means no expiration.")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
/*
|
||||
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 bearertoken
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
)
|
||||
|
||||
type Authenticator struct {
|
||||
auth authenticator.Token
|
||||
}
|
||||
|
||||
func New(auth authenticator.Token) *Authenticator {
|
||||
return &Authenticator{auth}
|
||||
}
|
||||
|
||||
var invalidToken = errors.New("invalid bearer token")
|
||||
|
||||
func (a *Authenticator) AuthenticateRequest(req *http.Request) (*authenticator.Response, bool, error) {
|
||||
auth := strings.TrimSpace(req.Header.Get("Authorization"))
|
||||
if auth == "" {
|
||||
return nil, false, nil
|
||||
}
|
||||
parts := strings.Split(auth, " ")
|
||||
if len(parts) < 2 || strings.ToLower(parts[0]) != "bearer" {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
token := parts[1]
|
||||
|
||||
// Empty bearer tokens aren't valid
|
||||
if len(token) == 0 {
|
||||
return nil, false, nil
|
||||
}
|
||||
|
||||
resp, ok, err := a.auth.AuthenticateToken(req.Context(), token)
|
||||
|
||||
// If the token authenticator didn't error, provide a default error
|
||||
if !ok && err == nil {
|
||||
err = invalidToken
|
||||
}
|
||||
|
||||
return resp, ok, err
|
||||
}
|
||||
@@ -0,0 +1,192 @@
|
||||
/*
|
||||
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 bearertoken
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"k8s.io/apiserver/pkg/authentication/authenticator"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
)
|
||||
|
||||
func TestAuthenticateRequest(t *testing.T) {
|
||||
auth := New(authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) {
|
||||
if token != "token" {
|
||||
t.Errorf("unexpected token: %s", token)
|
||||
}
|
||||
return &authenticator.Response{User: &user.DefaultInfo{Name: "user"}}, true, nil
|
||||
}))
|
||||
resp, ok, err := auth.AuthenticateRequest(&http.Request{
|
||||
Header: http.Header{"Authorization": []string{"Bearer token"}},
|
||||
})
|
||||
if !ok || resp == nil || err != nil {
|
||||
t.Errorf("expected valid user")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticateRequestTokenInvalid(t *testing.T) {
|
||||
auth := New(authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) {
|
||||
return nil, false, nil
|
||||
}))
|
||||
resp, ok, err := auth.AuthenticateRequest(&http.Request{
|
||||
Header: http.Header{"Authorization": []string{"Bearer token"}},
|
||||
})
|
||||
if ok || resp != nil {
|
||||
t.Errorf("expected not authenticated user")
|
||||
}
|
||||
if err != invalidToken {
|
||||
t.Errorf("expected invalidToken error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticateRequestTokenInvalidCustomError(t *testing.T) {
|
||||
customError := errors.New("custom")
|
||||
auth := New(authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) {
|
||||
return nil, false, customError
|
||||
}))
|
||||
resp, ok, err := auth.AuthenticateRequest(&http.Request{
|
||||
Header: http.Header{"Authorization": []string{"Bearer token"}},
|
||||
})
|
||||
if ok || resp != nil {
|
||||
t.Errorf("expected not authenticated user")
|
||||
}
|
||||
if err != customError {
|
||||
t.Errorf("expected custom error, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticateRequestTokenError(t *testing.T) {
|
||||
auth := New(authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) {
|
||||
return nil, false, errors.New("error")
|
||||
}))
|
||||
resp, ok, err := auth.AuthenticateRequest(&http.Request{
|
||||
Header: http.Header{"Authorization": []string{"Bearer token"}},
|
||||
})
|
||||
if ok || resp != nil || err == nil {
|
||||
t.Errorf("expected error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAuthenticateRequestBadValue(t *testing.T) {
|
||||
testCases := []struct {
|
||||
Req *http.Request
|
||||
}{
|
||||
{Req: &http.Request{}},
|
||||
{Req: &http.Request{Header: http.Header{"Authorization": []string{"Bearer"}}}},
|
||||
{Req: &http.Request{Header: http.Header{"Authorization": []string{"bear token"}}}},
|
||||
{Req: &http.Request{Header: http.Header{"Authorization": []string{"Bearer: token"}}}},
|
||||
}
|
||||
for i, testCase := range testCases {
|
||||
auth := New(authenticator.TokenFunc(func(ctx context.Context, token string) (*authenticator.Response, bool, error) {
|
||||
t.Errorf("authentication should not have been called")
|
||||
return nil, false, nil
|
||||
}))
|
||||
user, ok, err := auth.AuthenticateRequest(testCase.Req)
|
||||
if ok || user != nil || err != nil {
|
||||
t.Errorf("%d: expected not authenticated (no token)", i)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBearerToken(t *testing.T) {
|
||||
tests := map[string]struct {
|
||||
AuthorizationHeaders []string
|
||||
TokenAuth authenticator.Token
|
||||
|
||||
ExpectedUserName string
|
||||
ExpectedOK bool
|
||||
ExpectedErr bool
|
||||
ExpectedAuthorizationHeaders []string
|
||||
}{
|
||||
"no header": {
|
||||
AuthorizationHeaders: nil,
|
||||
ExpectedUserName: "",
|
||||
ExpectedOK: false,
|
||||
ExpectedErr: false,
|
||||
ExpectedAuthorizationHeaders: nil,
|
||||
},
|
||||
"empty header": {
|
||||
AuthorizationHeaders: []string{""},
|
||||
ExpectedUserName: "",
|
||||
ExpectedOK: false,
|
||||
ExpectedErr: false,
|
||||
ExpectedAuthorizationHeaders: []string{""},
|
||||
},
|
||||
"non-bearer header": {
|
||||
AuthorizationHeaders: []string{"Basic 123"},
|
||||
ExpectedUserName: "",
|
||||
ExpectedOK: false,
|
||||
ExpectedErr: false,
|
||||
ExpectedAuthorizationHeaders: []string{"Basic 123"},
|
||||
},
|
||||
"empty bearer token": {
|
||||
AuthorizationHeaders: []string{"Bearer "},
|
||||
ExpectedUserName: "",
|
||||
ExpectedOK: false,
|
||||
ExpectedErr: false,
|
||||
ExpectedAuthorizationHeaders: []string{"Bearer "},
|
||||
},
|
||||
"invalid bearer token": {
|
||||
AuthorizationHeaders: []string{"Bearer 123"},
|
||||
TokenAuth: authenticator.TokenFunc(func(ctx context.Context, t string) (*authenticator.Response, bool, error) { return nil, false, nil }),
|
||||
ExpectedUserName: "",
|
||||
ExpectedOK: false,
|
||||
ExpectedErr: true,
|
||||
ExpectedAuthorizationHeaders: []string{"Bearer 123"},
|
||||
},
|
||||
"error bearer token": {
|
||||
AuthorizationHeaders: []string{"Bearer 123"},
|
||||
TokenAuth: authenticator.TokenFunc(func(ctx context.Context, t string) (*authenticator.Response, bool, error) {
|
||||
return nil, false, errors.New("error")
|
||||
}),
|
||||
ExpectedUserName: "",
|
||||
ExpectedOK: false,
|
||||
ExpectedErr: true,
|
||||
ExpectedAuthorizationHeaders: []string{"Bearer 123"},
|
||||
},
|
||||
}
|
||||
|
||||
for k, tc := range tests {
|
||||
req, _ := http.NewRequest("GET", "/", nil)
|
||||
for _, h := range tc.AuthorizationHeaders {
|
||||
req.Header.Add("Authorization", h)
|
||||
}
|
||||
|
||||
bearerAuth := New(tc.TokenAuth)
|
||||
resp, ok, err := bearerAuth.AuthenticateRequest(req)
|
||||
if tc.ExpectedErr != (err != nil) {
|
||||
t.Errorf("%s: Expected err=%v, got %v", k, tc.ExpectedErr, err)
|
||||
continue
|
||||
}
|
||||
if ok != tc.ExpectedOK {
|
||||
t.Errorf("%s: Expected ok=%v, got %v", k, tc.ExpectedOK, ok)
|
||||
continue
|
||||
}
|
||||
if ok && resp.User.GetName() != tc.ExpectedUserName {
|
||||
t.Errorf("%s: Expected username=%v, got %v", k, tc.ExpectedUserName, resp.User.GetName())
|
||||
continue
|
||||
}
|
||||
if !reflect.DeepEqual(req.Header["Authorization"], tc.ExpectedAuthorizationHeaders) {
|
||||
t.Errorf("%s: Expected headers=%#v, got %#v", k, tc.ExpectedAuthorizationHeaders, req.Header["Authorization"])
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,8 @@ package token
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/dgrijalva/jwt-go"
|
||||
"kubesphere.io/kubesphere/pkg/api/iam"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"k8s.io/klog"
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
"kubesphere.io/kubesphere/pkg/server/errors"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/cache"
|
||||
@@ -53,23 +54,29 @@ func (s *jwtTokenIssuer) Verify(tokenString string) (User, error) {
|
||||
if len(tokenString) == 0 {
|
||||
return nil, errInvalidToken
|
||||
}
|
||||
_, err := s.cache.Get(tokenCacheKey(tokenString))
|
||||
|
||||
if err != nil {
|
||||
if err == cache.ErrNoSuchKey {
|
||||
return nil, errTokenExpired
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
clm := &Claims{}
|
||||
|
||||
_, err = jwt.ParseWithClaims(tokenString, clm, s.keyFunc)
|
||||
_, err := jwt.ParseWithClaims(tokenString, clm, s.keyFunc)
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &iam.User{Name: clm.Username, UID: clm.UID}, nil
|
||||
// 0 means no expiration.
|
||||
// validate token cache
|
||||
if s.options.OAuthOptions.AccessTokenMaxAge > 0 {
|
||||
_, err = s.cache.Get(tokenCacheKey(tokenString))
|
||||
|
||||
if err != nil {
|
||||
if err == cache.ErrNoSuchKey {
|
||||
return nil, errTokenExpired
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
return &user.DefaultInfo{Name: clm.Username, UID: clm.UID}, nil
|
||||
}
|
||||
|
||||
func (s *jwtTokenIssuer) IssueTo(user User, expiresIn time.Duration) (string, error) {
|
||||
@@ -92,16 +99,28 @@ func (s *jwtTokenIssuer) IssueTo(user User, expiresIn time.Duration) (string, er
|
||||
tokenString, err := token.SignedString([]byte(s.options.JwtSecret))
|
||||
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
s.cache.Set(tokenCacheKey(tokenString), tokenString, expiresIn)
|
||||
// 0 means no expiration.
|
||||
// validate token cache
|
||||
if s.options.OAuthOptions.AccessTokenMaxAge > 0 {
|
||||
err = s.cache.Set(tokenCacheKey(tokenString), tokenString, s.options.OAuthOptions.AccessTokenMaxAge)
|
||||
if err != nil {
|
||||
klog.Error(err)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
return tokenString, nil
|
||||
}
|
||||
|
||||
func (s *jwtTokenIssuer) Revoke(token string) error {
|
||||
return s.cache.Del(tokenCacheKey(token))
|
||||
if s.options.OAuthOptions.AccessTokenMaxAge > 0 {
|
||||
return s.cache.Del(tokenCacheKey(token))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func NewJwtTokenIssuer(issuerName string, options *authoptions.AuthenticationOptions, cache cache.Interface) Issuer {
|
||||
|
||||
@@ -20,7 +20,8 @@ package token
|
||||
|
||||
import (
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"kubesphere.io/kubesphere/pkg/api/iam"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authentication/oauth"
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/cache"
|
||||
"testing"
|
||||
@@ -48,7 +49,7 @@ func TestJwtTokenIssuer(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, testCase := range testCases {
|
||||
user := &iam.User{
|
||||
user := &user.DefaultInfo{
|
||||
Name: testCase.name,
|
||||
UID: testCase.uid,
|
||||
}
|
||||
@@ -70,3 +71,39 @@ func TestJwtTokenIssuer(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenVerifyWithoutCacheValidate(t *testing.T) {
|
||||
options := authoptions.NewAuthenticateOptions()
|
||||
|
||||
// do not set token cache and disable token cache validate,
|
||||
options.OAuthOptions = &oauth.Options{AccessTokenMaxAge: 0}
|
||||
options.JwtSecret = "kubesphere"
|
||||
issuer := NewJwtTokenIssuer(DefaultIssuerName, options, nil)
|
||||
|
||||
client, err := options.OAuthOptions.OAuthClient("default")
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
user := &user.DefaultInfo{
|
||||
Name: "admin",
|
||||
UID: "admin",
|
||||
}
|
||||
|
||||
tokenString, err := issuer.IssueTo(user, *client.AccessTokenMaxAge)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
got, err := issuer.Verify(tokenString)
|
||||
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if diff := cmp.Diff(got, user); diff != "" {
|
||||
t.Error("token validate failed")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,9 @@ package authorizerfactory
|
||||
import (
|
||||
"context"
|
||||
"github.com/open-policy-agent/opa/rego"
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/klog"
|
||||
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
|
||||
"kubesphere.io/kubesphere/pkg/models/iam/am"
|
||||
)
|
||||
@@ -29,115 +32,126 @@ type opaAuthorizer struct {
|
||||
am am.AccessManagementInterface
|
||||
}
|
||||
|
||||
const (
|
||||
permissionUndefined = "permission undefined"
|
||||
defaultRegoQuery = "data.authz.allow"
|
||||
)
|
||||
|
||||
// Make decision by request attributes
|
||||
func (o *opaAuthorizer) Authorize(attr authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
|
||||
// Make decisions based on the authorization policy of different levels of roles
|
||||
platformRole, err := o.am.GetPlatformRole(attr.GetUser().GetName())
|
||||
// Error returned when an internal error occurs
|
||||
// Reason must be returned when access is denied
|
||||
globalRole, err := o.am.GetRoleOfUserInTargetScope(iamv1alpha2.GlobalScope, "", attr.GetUser().GetName())
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return authorizer.DecisionDeny, err.Error(), nil
|
||||
}
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
|
||||
// check platform role policy rules
|
||||
if authorized, reason, err = makeDecision(platformRole, attr); authorized == authorizer.DecisionAllow {
|
||||
return authorized, reason, err
|
||||
if authorized, reason, err = o.makeDecision(globalRole, attr); authorized == authorizer.DecisionAllow {
|
||||
return authorized, reason, nil
|
||||
}
|
||||
|
||||
// it's not in cluster resource, permission denied
|
||||
if attr.GetCluster() == "" {
|
||||
return authorizer.DecisionDeny, "permission undefined", nil
|
||||
return authorizer.DecisionDeny, permissionUndefined, nil
|
||||
}
|
||||
|
||||
clusterRole, err := o.am.GetClusterRole(attr.GetCluster(), attr.GetUser().GetName())
|
||||
clusterRole, err := o.am.GetRoleOfUserInTargetScope(iamv1alpha2.ClusterScope, attr.GetCluster(), attr.GetUser().GetName())
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return authorizer.DecisionDeny, err.Error(), nil
|
||||
}
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
|
||||
// check cluster role policy rules
|
||||
if a, r, e := makeDecision(clusterRole, attr); a == authorizer.DecisionAllow {
|
||||
return a, r, e
|
||||
if authorized, reason, err := o.makeDecision(clusterRole, attr); authorized == authorizer.DecisionAllow {
|
||||
return authorized, reason, nil
|
||||
} else if err != nil {
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
|
||||
// it's not in cluster resource, permission denied
|
||||
if attr.GetWorkspace() == "" && attr.GetNamespace() == "" {
|
||||
return authorizer.DecisionDeny, "permission undefined", nil
|
||||
return authorizer.DecisionDeny, permissionUndefined, nil
|
||||
}
|
||||
|
||||
workspaceRole, err := o.am.GetWorkspaceRole(attr.GetWorkspace(), attr.GetUser().GetName())
|
||||
workspaceRole, err := o.am.GetRoleOfUserInTargetScope(iamv1alpha2.WorkspaceScope, attr.GetWorkspace(), attr.GetUser().GetName())
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return authorizer.DecisionDeny, err.Error(), nil
|
||||
}
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
|
||||
// check workspace role policy rules
|
||||
if a, r, e := makeDecision(workspaceRole, attr); a == authorizer.DecisionAllow {
|
||||
return a, r, e
|
||||
if authorized, reason, err := o.makeDecision(workspaceRole, attr); authorized == authorizer.DecisionAllow {
|
||||
return authorized, reason, err
|
||||
} else if err != nil {
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
|
||||
// it's not in workspace resource, permission denied
|
||||
if attr.GetNamespace() == "" {
|
||||
return authorizer.DecisionDeny, "permission undefined", nil
|
||||
return authorizer.DecisionDeny, permissionUndefined, nil
|
||||
}
|
||||
|
||||
if attr.GetNamespace() != "" {
|
||||
namespaceRole, err := o.am.GetNamespaceRole(attr.GetCluster(), attr.GetNamespace(), attr.GetUser().GetName())
|
||||
if err != nil {
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
// check namespace role policy rules
|
||||
if a, r, e := makeDecision(namespaceRole, attr); a == authorizer.DecisionAllow {
|
||||
return a, r, e
|
||||
namespaceRole, err := o.am.GetRoleOfUserInTargetScope(iamv1alpha2.NamespaceScope, attr.GetNamespace(), attr.GetUser().GetName())
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
return authorizer.DecisionDeny, err.Error(), nil
|
||||
}
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
// check namespace role policy rules
|
||||
if authorized, reason, err := o.makeDecision(namespaceRole, attr); authorized == authorizer.DecisionAllow {
|
||||
return authorized, reason, err
|
||||
} else if err != nil {
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
|
||||
return authorizer.DecisionDeny, "", nil
|
||||
return authorizer.DecisionDeny, permissionUndefined, nil
|
||||
}
|
||||
|
||||
// Make decision base on role
|
||||
func makeDecision(role am.Role, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
func (o *opaAuthorizer) makeDecision(role *iamv1alpha2.Role, a authorizer.Attributes) (authorized authorizer.Decision, reason string, err error) {
|
||||
|
||||
// Call the rego.New function to create an object that can be prepared or evaluated
|
||||
// After constructing a new rego.Rego object you can call PrepareForEval() to obtain an executable query
|
||||
query, err := rego.New(rego.Query("data.authz.allow"), rego.Module("authz.rego", role.GetRego())).PrepareForEval(context.Background())
|
||||
for _, ruleRef := range role.Rules {
|
||||
rule, err := o.am.GetPolicyRule(ruleRef.Name)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
continue
|
||||
}
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
// Call the rego.New function to create an object that can be prepared or evaluated
|
||||
// After constructing a new rego.Rego object you can call PrepareForEval() to obtain an executable query
|
||||
query, err := rego.New(rego.Query(defaultRegoQuery), rego.Module("authz.rego", rule.Rego)).PrepareForEval(context.Background())
|
||||
|
||||
if err != nil {
|
||||
return authorizer.DecisionDeny, "", err
|
||||
if err != nil {
|
||||
klog.Errorf("rule syntax error:%s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// The policy decision is contained in the results returned by the Eval() call. You can inspect the decision and handle it accordingly.
|
||||
results, err := query.Eval(context.Background(), rego.EvalInput(a))
|
||||
|
||||
if err != nil {
|
||||
klog.Errorf("rule syntax error:%s", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if len(results) > 0 && results[0].Expressions[0].Value == true {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
}
|
||||
|
||||
// data example
|
||||
//{
|
||||
// "User": {
|
||||
// "Name": "admin",
|
||||
// "UID": "0",
|
||||
// "Groups": [
|
||||
// "admin"
|
||||
// ],
|
||||
// "Extra": null
|
||||
// },
|
||||
// "Verb": "list",
|
||||
// "Cluster": "cluster1",
|
||||
// "Workspace": "",
|
||||
// "Namespace": "",
|
||||
// "APIGroup": "",
|
||||
// "APIVersion": "v1",
|
||||
// "Resource": "nodes",
|
||||
// "Subresource": "",
|
||||
// "Name": "",
|
||||
// "KubernetesRequest": true,
|
||||
// "ResourceRequest": true,
|
||||
// "Path": "/api/v1/nodes"
|
||||
//}
|
||||
// The policy decision is contained in the results returned by the Eval() call. You can inspect the decision and handle it accordingly.
|
||||
results, err := query.Eval(context.Background(), rego.EvalInput(a))
|
||||
|
||||
if err != nil {
|
||||
return authorizer.DecisionDeny, "", err
|
||||
}
|
||||
|
||||
if len(results) > 0 && results[0].Expressions[0].Value == true {
|
||||
return authorizer.DecisionAllow, "", nil
|
||||
}
|
||||
|
||||
return authorizer.DecisionDeny, "permission undefined", nil
|
||||
return authorizer.DecisionDeny, permissionUndefined, nil
|
||||
}
|
||||
|
||||
func NewOPAAuthorizer(am am.AccessManagementInterface) *opaAuthorizer {
|
||||
|
||||
@@ -19,22 +19,44 @@
|
||||
package authorizerfactory
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
|
||||
"k8s.io/apiserver/pkg/authentication/user"
|
||||
iamvealpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
|
||||
"kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake"
|
||||
"kubesphere.io/kubesphere/pkg/client/informers/externalversions"
|
||||
"kubesphere.io/kubesphere/pkg/models/iam/am"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPlatformRole(t *testing.T) {
|
||||
platformRoles := map[string]am.FakeRole{"admin": {
|
||||
Name: "admin",
|
||||
Rego: "package authz\ndefault allow = true",
|
||||
}, "anonymous": {
|
||||
Name: "anonymous",
|
||||
Rego: "package authz\ndefault allow = false",
|
||||
}, "tom": {
|
||||
Name: "tom",
|
||||
Rego: `package authz
|
||||
func prepare() (am.AccessManagementInterface, error) {
|
||||
rules := []*iamvealpha2.PolicyRule{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.PolicyRuleKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "always-allow",
|
||||
},
|
||||
Rego: "package authz\ndefault allow = true",
|
||||
}, {
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.PolicyRuleKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String(),
|
||||
},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "always-deny",
|
||||
},
|
||||
Rego: "package authz\ndefault allow = false",
|
||||
}, {
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.PolicyRuleKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "manage-cluster1-resources",
|
||||
},
|
||||
Rego: `package authz
|
||||
default allow = false
|
||||
allow {
|
||||
resources_in_cluster1
|
||||
@@ -42,11 +64,168 @@ allow {
|
||||
resources_in_cluster1 {
|
||||
input.Cluster == "cluster1"
|
||||
}`,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
operator := am.NewFakeAMOperator()
|
||||
operator.Prepare(platformRoles, nil, nil, nil)
|
||||
roles := []*iamvealpha2.Role{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.RoleKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "global-admin",
|
||||
},
|
||||
Target: iamvealpha2.Target{
|
||||
Scope: iamvealpha2.GlobalScope,
|
||||
Name: "",
|
||||
},
|
||||
Rules: []iamvealpha2.RuleRef{
|
||||
{
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Kind: iamvealpha2.PolicyRuleKind,
|
||||
Name: "always-allow",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.RoleKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "anonymous",
|
||||
},
|
||||
Target: iamvealpha2.Target{
|
||||
Scope: iamvealpha2.GlobalScope,
|
||||
Name: "",
|
||||
},
|
||||
Rules: []iamvealpha2.RuleRef{
|
||||
{
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Kind: iamvealpha2.PolicyRuleKind,
|
||||
Name: "always-deny",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.RoleKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "cluster1-admin",
|
||||
},
|
||||
Target: iamvealpha2.Target{
|
||||
Scope: iamvealpha2.GlobalScope,
|
||||
Name: "",
|
||||
},
|
||||
Rules: []iamvealpha2.RuleRef{
|
||||
{
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Kind: iamvealpha2.PolicyRuleKind,
|
||||
Name: "manage-cluster1-resources",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
roleBindings := []*iamvealpha2.RoleBinding{
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.RoleBindingKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "global-admin",
|
||||
},
|
||||
Scope: iamvealpha2.GlobalScope,
|
||||
RoleRef: iamvealpha2.RoleRef{
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Kind: iamvealpha2.RoleKind,
|
||||
Name: "global-admin",
|
||||
},
|
||||
Subjects: []iamvealpha2.Subject{
|
||||
{
|
||||
Kind: iamvealpha2.UserKind,
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Name: "admin",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.RoleBindingKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "anonymous",
|
||||
},
|
||||
Scope: iamvealpha2.GlobalScope,
|
||||
RoleRef: iamvealpha2.RoleRef{
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Kind: iamvealpha2.RoleKind,
|
||||
Name: "anonymous",
|
||||
},
|
||||
Subjects: []iamvealpha2.Subject{
|
||||
{
|
||||
Kind: iamvealpha2.UserKind,
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Name: user.Anonymous,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
TypeMeta: metav1.TypeMeta{
|
||||
Kind: iamvealpha2.RoleBindingKind,
|
||||
APIVersion: iamvealpha2.SchemeGroupVersion.String()},
|
||||
ObjectMeta: metav1.ObjectMeta{
|
||||
Name: "cluster1-admin",
|
||||
},
|
||||
Scope: iamvealpha2.GlobalScope,
|
||||
RoleRef: iamvealpha2.RoleRef{
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Kind: iamvealpha2.RoleKind,
|
||||
Name: "cluster1-admin",
|
||||
},
|
||||
Subjects: []iamvealpha2.Subject{
|
||||
{
|
||||
Kind: iamvealpha2.UserKind,
|
||||
APIGroup: iamvealpha2.SchemeGroupVersion.String(),
|
||||
Name: "tom",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
ksClient := fake.NewSimpleClientset()
|
||||
informerFactory := externalversions.NewSharedInformerFactory(ksClient, 0)
|
||||
|
||||
for _, rule := range rules {
|
||||
err := informerFactory.Iam().V1alpha2().PolicyRules().Informer().GetIndexer().Add(rule)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("add rule:%s", err)
|
||||
}
|
||||
}
|
||||
for _, role := range roles {
|
||||
err := informerFactory.Iam().V1alpha2().Roles().Informer().GetIndexer().Add(role)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("add role:%s", err)
|
||||
}
|
||||
}
|
||||
for _, roleBinding := range roleBindings {
|
||||
err := informerFactory.Iam().V1alpha2().RoleBindings().Informer().GetIndexer().Add(roleBinding)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("add role binding:%s", err)
|
||||
}
|
||||
}
|
||||
|
||||
operator := am.NewAMOperator(ksClient, informerFactory)
|
||||
|
||||
return operator, nil
|
||||
}
|
||||
|
||||
func TestGlobalRole(t *testing.T) {
|
||||
|
||||
operator, err := prepare()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
opa := NewOPAAuthorizer(operator)
|
||||
|
||||
|
||||
@@ -2,11 +2,8 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/emicklei/go-restful"
|
||||
"github.com/spf13/viper"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/runtime"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/alerting"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/cache"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/devops/jenkins"
|
||||
@@ -19,7 +16,6 @@ import (
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/s3"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/servicemesh"
|
||||
"kubesphere.io/kubesphere/pkg/simple/client/sonarqube"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"strings"
|
||||
)
|
||||
@@ -123,26 +119,6 @@ func TryLoadFromDisk() (*Config, error) {
|
||||
return conf, nil
|
||||
}
|
||||
|
||||
// InstallAPI installs api for config
|
||||
func (conf *Config) InstallAPI(c *restful.Container) {
|
||||
ws := runtime.NewWebService(schema.GroupVersion{
|
||||
Group: "",
|
||||
Version: "v1alpha1",
|
||||
})
|
||||
|
||||
ws.Route(ws.GET("/configz").
|
||||
To(func(request *restful.Request, response *restful.Response) {
|
||||
conf.stripEmptyOptions()
|
||||
response.WriteAsJson(conf.ToMap())
|
||||
}).
|
||||
Doc("Get system components configuration").
|
||||
Produces(restful.MIME_JSON).
|
||||
Writes(Config{}).
|
||||
Returns(http.StatusOK, "ok", Config{}))
|
||||
|
||||
c.Add(ws)
|
||||
}
|
||||
|
||||
// convertToMap simply converts config to map[string]bool
|
||||
// to hide sensitive information
|
||||
func (conf *Config) ToMap() map[string]bool {
|
||||
|
||||
@@ -6,39 +6,59 @@ import (
|
||||
"k8s.io/apimachinery/pkg/api/errors"
|
||||
"k8s.io/apimachinery/pkg/util/proxy"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
towerv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tower/v1alpha1"
|
||||
"k8s.io/klog"
|
||||
clusterv1alpha1 "kubesphere.io/kubesphere/pkg/apis/cluster/v1alpha1"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/request"
|
||||
"kubesphere.io/kubesphere/pkg/client/listers/tower/v1alpha1"
|
||||
"kubesphere.io/kubesphere/pkg/client/listers/cluster/v1alpha1"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const defaultMultipleClusterAgentNamespace = "kubesphere-system"
|
||||
|
||||
// Dispatcher defines how to forward request to designated cluster based on cluster name
|
||||
type Dispatcher interface {
|
||||
Dispatch(w http.ResponseWriter, req *http.Request, handler http.Handler)
|
||||
}
|
||||
|
||||
type clusterDispatch struct {
|
||||
agentLister v1alpha1.AgentLister
|
||||
agentLister v1alpha1.AgentLister
|
||||
clusterLister v1alpha1.ClusterLister
|
||||
}
|
||||
|
||||
func NewClusterDispatch(agentLister v1alpha1.AgentLister) Dispatcher {
|
||||
func NewClusterDispatch(agentLister v1alpha1.AgentLister, clusterLister v1alpha1.ClusterLister) Dispatcher {
|
||||
return &clusterDispatch{
|
||||
agentLister: agentLister,
|
||||
agentLister: agentLister,
|
||||
clusterLister: clusterLister,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *clusterDispatch) Dispatch(w http.ResponseWriter, req *http.Request, handler http.Handler) {
|
||||
|
||||
info, _ := request.RequestInfoFrom(req.Context())
|
||||
if info.Cluster == "" { // fallback to host cluster if cluster name if empty
|
||||
|
||||
if len(info.Cluster) == 0 {
|
||||
klog.Warningf("Request with empty cluster, %v", req.URL)
|
||||
http.Error(w, fmt.Sprintf("Bad request, empty cluster"), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
cluster, err := c.clusterLister.Get(info.Cluster)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
http.Error(w, fmt.Sprintf("cluster %s not found", info.Cluster), http.StatusNotFound)
|
||||
} else {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// request cluster is host cluster, no need go through agent
|
||||
if isClusterHostCluster(cluster) {
|
||||
req.URL.Path = strings.Replace(req.URL.Path, fmt.Sprintf("/clusters/%s", info.Cluster), "", 1)
|
||||
handler.ServeHTTP(w, req)
|
||||
return
|
||||
}
|
||||
|
||||
agent, err := c.agentLister.Agents(defaultMultipleClusterAgentNamespace).Get(info.Cluster)
|
||||
agent, err := c.agentLister.Get(info.Cluster)
|
||||
if err != nil {
|
||||
if errors.IsNotFound(err) {
|
||||
http.Error(w, fmt.Sprintf("cluster %s not found", info.Cluster), http.StatusNotFound)
|
||||
@@ -54,7 +74,7 @@ func (c *clusterDispatch) Dispatch(w http.ResponseWriter, req *http.Request, han
|
||||
}
|
||||
|
||||
u := *req.URL
|
||||
u.Host = fmt.Sprintf("%s:%d", agent.Spec.Proxy, agent.Spec.KubeSphereAPIServerPort)
|
||||
u.Host = agent.Spec.Proxy
|
||||
u.Path = strings.Replace(u.Path, fmt.Sprintf("/clusters/%s", info.Cluster), "", 1)
|
||||
|
||||
httpProxy := proxy.NewUpgradeAwareHandler(&u, http.DefaultTransport, true, false, c)
|
||||
@@ -65,9 +85,20 @@ func (c *clusterDispatch) Error(w http.ResponseWriter, req *http.Request, err er
|
||||
responsewriters.InternalError(w, req, err)
|
||||
}
|
||||
|
||||
func isAgentReady(agent *towerv1alpha1.Agent) bool {
|
||||
func isAgentReady(agent *clusterv1alpha1.Agent) bool {
|
||||
for _, condition := range agent.Status.Conditions {
|
||||
if condition.Type == towerv1alpha1.AgentConnected && condition.Status == corev1.ConditionTrue {
|
||||
if condition.Type == clusterv1alpha1.AgentConnected && condition.Status == corev1.ConditionTrue {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
//
|
||||
func isClusterHostCluster(cluster *clusterv1alpha1.Cluster) bool {
|
||||
for key, value := range cluster.Annotations {
|
||||
if key == clusterv1alpha1.IsHostCluster && value == "true" {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,9 +42,6 @@ func WithAuthentication(handler http.Handler, auth authenticator.Request) http.H
|
||||
return
|
||||
}
|
||||
|
||||
// authorization header is not required anymore in case of a successful authentication.
|
||||
req.Header.Del("Authorization")
|
||||
|
||||
req = req.WithContext(request.WithUser(req.Context(), resp.User))
|
||||
handler.ServeHTTP(w, req)
|
||||
})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
package filters
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"k8s.io/apimachinery/pkg/util/proxy"
|
||||
"k8s.io/apiserver/pkg/endpoints/handlers/responsewriters"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/klog"
|
||||
@@ -9,9 +9,6 @@ import (
|
||||
"kubesphere.io/kubesphere/pkg/server/errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/proxy"
|
||||
)
|
||||
|
||||
// WithKubeAPIServer proxy request to kubernetes service if requests path starts with /api
|
||||
@@ -35,8 +32,9 @@ 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)
|
||||
|
||||
// Do not cover k8s client authorization header
|
||||
req.Header.Del("Authorization")
|
||||
|
||||
httpProxy := proxy.NewUpgradeAwareHandler(&s, defaultTransport, true, false, failed)
|
||||
httpProxy.ServeHTTP(w, req)
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
package query
|
||||
|
||||
type Field string
|
||||
type Value string
|
||||
|
||||
const (
|
||||
FieldName = "name"
|
||||
FieldUID = "uid"
|
||||
FieldCreationTimeStamp = "creationTimestamp"
|
||||
FieldLastUpdateTimestamp = "lastUpdateTimestamp"
|
||||
FieldLabel = "label"
|
||||
FieldAnnotation = "annotation"
|
||||
FieldNamespace = "namespace"
|
||||
FieldStatus = "status"
|
||||
FieldApplication = "application"
|
||||
FieldOwner = "owner"
|
||||
FieldOwnerReference = "ownerReference"
|
||||
FieldOwnerKind = "ownerKind"
|
||||
)
|
||||
|
||||
@@ -23,9 +25,11 @@ var SortableFields = []Field{
|
||||
// Field contains all the query field that can be compared
|
||||
var ComparableFields = []Field{
|
||||
FieldName,
|
||||
FieldUID,
|
||||
FieldLabel,
|
||||
FieldAnnotation,
|
||||
FieldNamespace,
|
||||
FieldStatus,
|
||||
FieldApplication,
|
||||
FieldOwner,
|
||||
FieldOwnerReference,
|
||||
FieldOwnerKind,
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@ package query
|
||||
import (
|
||||
"github.com/emicklei/go-restful"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -16,24 +15,6 @@ const (
|
||||
ParameterAscending = "ascending"
|
||||
)
|
||||
|
||||
type Comparable interface {
|
||||
Compare(Comparable) int
|
||||
|
||||
Contains(Comparable) bool
|
||||
}
|
||||
|
||||
type ComparableString string
|
||||
|
||||
func (c ComparableString) Compare(comparable Comparable) int {
|
||||
other := comparable.(ComparableString)
|
||||
return strings.Compare(string(c), string(other))
|
||||
}
|
||||
|
||||
func (c ComparableString) Contains(comparable Comparable) bool {
|
||||
other := comparable.(ComparableString)
|
||||
return strings.Contains(string(c), string(other))
|
||||
}
|
||||
|
||||
// Query represents api search terms
|
||||
type Query struct {
|
||||
Pagination *Pagination
|
||||
@@ -52,29 +33,30 @@ type Pagination struct {
|
||||
// items per page
|
||||
Limit int
|
||||
|
||||
// page number
|
||||
Page int
|
||||
// offset
|
||||
Offset int
|
||||
}
|
||||
|
||||
var NoPagination = newPagination(-1, -1)
|
||||
var NoPagination = newPagination(-1, 0)
|
||||
|
||||
func newPagination(limit int, page int) *Pagination {
|
||||
// make sure that pagination is valid
|
||||
func newPagination(limit int, offset int) *Pagination {
|
||||
return &Pagination{
|
||||
Limit: limit,
|
||||
Page: page,
|
||||
Limit: limit,
|
||||
Offset: offset,
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Pagination) IsValidPagintaion() bool {
|
||||
return p.Limit >= 0 && p.Page >= 0
|
||||
}
|
||||
func (p *Pagination) GetValidPagination(total int) (startIndex, endIndex int) {
|
||||
if p.Limit == NoPagination.Limit {
|
||||
return 0, total
|
||||
}
|
||||
|
||||
func (p *Pagination) IsPageAvailable(total, startIndex int) bool {
|
||||
return total > startIndex && p.Limit > 0
|
||||
}
|
||||
if p.Limit < 0 || p.Offset < 0 {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func (p *Pagination) GetPaginationSettings(total int) (startIndex, endIndex int) {
|
||||
startIndex = p.Limit * p.Page
|
||||
startIndex = p.Limit * p.Offset
|
||||
endIndex = startIndex + p.Limit
|
||||
|
||||
if endIndex > total {
|
||||
@@ -86,33 +68,33 @@ func (p *Pagination) GetPaginationSettings(total int) (startIndex, endIndex int)
|
||||
|
||||
func New() *Query {
|
||||
return &Query{
|
||||
Pagination: &Pagination{
|
||||
Limit: -1,
|
||||
Page: -1,
|
||||
},
|
||||
SortBy: "",
|
||||
Ascending: false,
|
||||
Filters: []Filter{},
|
||||
Pagination: NoPagination,
|
||||
SortBy: "",
|
||||
Ascending: false,
|
||||
Filters: []Filter{},
|
||||
}
|
||||
}
|
||||
|
||||
type Filter struct {
|
||||
Field Field
|
||||
Value Comparable
|
||||
Value Value
|
||||
}
|
||||
|
||||
func ParseQueryParameter(request *restful.Request) *Query {
|
||||
query := New()
|
||||
|
||||
limit, err := strconv.ParseInt(request.QueryParameter("limit"), 10, 0)
|
||||
limit, err := strconv.Atoi(request.QueryParameter("limit"))
|
||||
// equivalent to undefined, use the default value
|
||||
if err != nil {
|
||||
query.Pagination = NoPagination
|
||||
limit = -1
|
||||
}
|
||||
page, err := strconv.Atoi(request.QueryParameter("page"))
|
||||
// equivalent to undefined, use the default value
|
||||
if err != nil {
|
||||
page = 1
|
||||
}
|
||||
|
||||
page, err := strconv.ParseInt(request.QueryParameter("page"), 10, 0)
|
||||
if err == nil {
|
||||
query.Pagination = newPagination(int(limit), int(page-1))
|
||||
}
|
||||
query.Pagination = newPagination(limit, page-1)
|
||||
|
||||
query.SortBy = Field(defaultString(request.QueryParameter("sortBy"), FieldCreationTimeStamp))
|
||||
|
||||
@@ -128,13 +110,12 @@ func ParseQueryParameter(request *restful.Request) *Query {
|
||||
if len(f) != 0 {
|
||||
query.Filters = append(query.Filters, Filter{
|
||||
Field: field,
|
||||
Value: ComparableString(f),
|
||||
Value: Value(f),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return query
|
||||
|
||||
}
|
||||
|
||||
func defaultString(value, defaultValue string) string {
|
||||
|
||||
@@ -16,7 +16,7 @@ func TestParseQueryParameter(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
"test normal case",
|
||||
"name=foo&status=Running&application=book&page=1&limit=10&ascending=true",
|
||||
"label=app.kubernetes.io/name:book&name=foo&status=Running&page=1&limit=10&ascending=true",
|
||||
&Query{
|
||||
Pagination: newPagination(10, 0),
|
||||
SortBy: FieldCreationTimeStamp,
|
||||
@@ -24,15 +24,15 @@ func TestParseQueryParameter(t *testing.T) {
|
||||
Filters: []Filter{
|
||||
{
|
||||
FieldName,
|
||||
ComparableString("foo"),
|
||||
Value("foo"),
|
||||
},
|
||||
{
|
||||
FieldLabel,
|
||||
Value("app.kubernetes.io/name:book"),
|
||||
},
|
||||
{
|
||||
FieldStatus,
|
||||
ComparableString("Running"),
|
||||
},
|
||||
{
|
||||
FieldApplication,
|
||||
ComparableString("book"),
|
||||
Value("Running"),
|
||||
},
|
||||
},
|
||||
},
|
||||
@@ -41,13 +41,10 @@ func TestParseQueryParameter(t *testing.T) {
|
||||
"test bad case",
|
||||
"xxxx=xxxx&dsfsw=xxxx&page=abc&limit=add&ascending=ssss",
|
||||
&Query{
|
||||
Pagination: &Pagination{
|
||||
Limit: -1,
|
||||
Page: -1,
|
||||
},
|
||||
SortBy: FieldCreationTimeStamp,
|
||||
Ascending: false,
|
||||
Filters: []Filter{},
|
||||
Pagination: NoPagination,
|
||||
SortBy: FieldCreationTimeStamp,
|
||||
Ascending: false,
|
||||
Filters: []Filter{},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@@ -78,8 +78,8 @@ type RequestInfoFactory struct {
|
||||
// /kapis/{api-group}/{version}/namespaces/{namespace}/{resource}
|
||||
// /kapis/{api-group}/{version}/namespaces/{namespace}/{resource}/{resourceName}
|
||||
// With workspaces:
|
||||
// /kapis/{api-group}/{version}/clusters/{cluster}/namespaces/{namespace}/{resource}
|
||||
// /kapis/{api-group}/{version}/clusters/{cluster}/namespaces/{namespace}/{resource}/{resourceName}
|
||||
// /kapis/clusters/{cluster}/{api-group}/{version}/namespaces/{namespace}/{resource}
|
||||
// /kapis/clusters/{cluster}/{api-group}/{version}/namespaces/{namespace}/{resource}/{resourceName}
|
||||
//
|
||||
func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, error) {
|
||||
|
||||
@@ -111,6 +111,16 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
|
||||
requestInfo.APIPrefix = currentParts[0]
|
||||
currentParts = currentParts[1:]
|
||||
|
||||
// URL forms: /clusters/{cluster}/*
|
||||
if currentParts[0] == "clusters" {
|
||||
if len(currentParts) > 1 {
|
||||
requestInfo.Cluster = currentParts[1]
|
||||
}
|
||||
if len(currentParts) > 2 {
|
||||
currentParts = currentParts[2:]
|
||||
}
|
||||
}
|
||||
|
||||
if !r.GrouplessAPIPrefixes.Has(requestInfo.APIPrefix) {
|
||||
// one part (APIPrefix) has already been consumed, so this is actually "do we have four parts?"
|
||||
if len(currentParts) < 3 {
|
||||
@@ -150,16 +160,6 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er
|
||||
}
|
||||
}
|
||||
|
||||
// URL forms: /clusters/{cluster}/*
|
||||
if currentParts[0] == "clusters" {
|
||||
if len(currentParts) > 1 {
|
||||
requestInfo.Cluster = currentParts[1]
|
||||
}
|
||||
if len(currentParts) > 2 {
|
||||
currentParts = currentParts[2:]
|
||||
}
|
||||
}
|
||||
|
||||
// URL forms: /workspaces/{workspace}/*
|
||||
if currentParts[0] == "workspaces" {
|
||||
if len(currentParts) > 1 {
|
||||
|
||||
@@ -59,40 +59,18 @@ func TestRequestInfoFactory_NewRequestInfo(t *testing.T) {
|
||||
expectedKubernetesRequest: false,
|
||||
},
|
||||
{
|
||||
name: "list cluster roles",
|
||||
url: "/apis/rbac.authorization.k8s.io/v1/clusters/cluster1/clusterroles",
|
||||
name: "list clusterRoles of cluster gondor",
|
||||
url: "/apis/clusters/gondor/rbac.authorization.k8s.io/v1/clusterroles",
|
||||
method: http.MethodGet,
|
||||
expectedErr: nil,
|
||||
expectedVerb: "list",
|
||||
expectedResource: "clusterroles",
|
||||
expectedIsResourceRequest: true,
|
||||
expectedCluster: "cluster1",
|
||||
expectedCluster: "gondor",
|
||||
expectedKubernetesRequest: true,
|
||||
},
|
||||
{
|
||||
name: "list cluster nodes",
|
||||
url: "/api/v1/clusters/cluster1/nodes",
|
||||
method: http.MethodGet,
|
||||
expectedErr: nil,
|
||||
expectedVerb: "list",
|
||||
expectedResource: "nodes",
|
||||
expectedIsResourceRequest: true,
|
||||
expectedCluster: "cluster1",
|
||||
expectedKubernetesRequest: true,
|
||||
},
|
||||
{
|
||||
name: "list cluster nodes",
|
||||
url: "/api/v1/clusters/cluster1/nodes",
|
||||
method: http.MethodGet,
|
||||
expectedErr: nil,
|
||||
expectedVerb: "list",
|
||||
expectedResource: "nodes",
|
||||
expectedIsResourceRequest: true,
|
||||
expectedCluster: "cluster1",
|
||||
expectedKubernetesRequest: true,
|
||||
},
|
||||
{
|
||||
name: "list cluster nodes",
|
||||
name: "list nodes",
|
||||
url: "/api/v1/nodes",
|
||||
method: http.MethodGet,
|
||||
expectedErr: nil,
|
||||
@@ -103,15 +81,26 @@ func TestRequestInfoFactory_NewRequestInfo(t *testing.T) {
|
||||
expectedKubernetesRequest: true,
|
||||
},
|
||||
{
|
||||
name: "list roles",
|
||||
url: "/apis/rbac.authorization.k8s.io/v1/clusters/cluster1/namespaces/namespace1/roles",
|
||||
name: "list nodes of cluster gondor",
|
||||
url: "/api/clusters/gondor/v1/nodes",
|
||||
method: http.MethodGet,
|
||||
expectedErr: nil,
|
||||
expectedVerb: "list",
|
||||
expectedResource: "nodes",
|
||||
expectedIsResourceRequest: true,
|
||||
expectedCluster: "gondor",
|
||||
expectedKubernetesRequest: true,
|
||||
},
|
||||
{
|
||||
name: "list roles of cluster gondor",
|
||||
url: "/apis/clusters/gondor/rbac.authorization.k8s.io/v1/namespaces/namespace1/roles",
|
||||
method: http.MethodGet,
|
||||
expectedErr: nil,
|
||||
expectedVerb: "list",
|
||||
expectedResource: "roles",
|
||||
expectedIsResourceRequest: true,
|
||||
expectedNamespace: "namespace1",
|
||||
expectedCluster: "cluster1",
|
||||
expectedCluster: "gondor",
|
||||
expectedKubernetesRequest: true,
|
||||
},
|
||||
{
|
||||
@@ -139,17 +128,41 @@ func TestRequestInfoFactory_NewRequestInfo(t *testing.T) {
|
||||
expectedKubernetesRequest: false,
|
||||
},
|
||||
{
|
||||
name: "list namespaces",
|
||||
url: "/kapis/resources.kubesphere.io/v1alpha3/clusters/cluster1/workspaces/workspace1/namespaces",
|
||||
name: "list namespaces of cluster gondor",
|
||||
url: "/kapis/clusters/gondor/resources.kubesphere.io/v1alpha3/workspaces/workspace1/namespaces",
|
||||
method: http.MethodGet,
|
||||
expectedErr: nil,
|
||||
expectedVerb: "list",
|
||||
expectedResource: "namespaces",
|
||||
expectedIsResourceRequest: true,
|
||||
expectedWorkspace: "workspace1",
|
||||
expectedCluster: "cluster1",
|
||||
expectedCluster: "gondor",
|
||||
expectedKubernetesRequest: false,
|
||||
},
|
||||
{
|
||||
name: "list clusters",
|
||||
url: "/apis/cluster.kubesphere.io/v1alpha1/clusters",
|
||||
method: http.MethodGet,
|
||||
expectedErr: nil,
|
||||
expectedVerb: "list",
|
||||
expectedResource: "clusters",
|
||||
expectedIsResourceRequest: true,
|
||||
expectedWorkspace: "",
|
||||
expectedCluster: "",
|
||||
expectedKubernetesRequest: true,
|
||||
},
|
||||
{
|
||||
name: "get cluster gondor",
|
||||
url: "/apis/cluster.kubesphere.io/v1alpha1/clusters/gondor",
|
||||
method: http.MethodGet,
|
||||
expectedErr: nil,
|
||||
expectedVerb: "get",
|
||||
expectedResource: "clusters",
|
||||
expectedIsResourceRequest: true,
|
||||
expectedWorkspace: "",
|
||||
expectedCluster: "",
|
||||
expectedKubernetesRequest: true,
|
||||
},
|
||||
{
|
||||
name: "random query",
|
||||
url: "/foo/bar",
|
||||
|
||||
Reference in New Issue
Block a user