Repository: kubeclipper/kubeclipper
Pull request #376 (2022-11-11 10:08:53)

[master] Refactor downloader
This is an automated cherry-pick of #364

```release-note
None
```

kubeclipper-bot: [APPROVALNOTIFIER] This PR is **NOT APPROVED**

This pull-request has been approved by: *<a href="https://github.com/kubeclipper/kubeclipper/pull/376#" title="Author self-approved">kubeclipper-bot</a>*
To complete the [pull request process](https://git.k8s.io/community/contributors/guide/owners.md#the-code-review-process), please assign **x893675** after the PR has been reviewed.
You can assign the PR to them by writing `/assign @x893675` in a comment when ready.

The full list of commands accepted by this bot can be found [here](https://go.k8s.io/bot-commands?repo=kubeclipper%2Fkubeclipper).

<details open>
Needs approval from an approver in each of these files:

- **[OWNERS](https://github.com/kubeclipper/kubeclipper/blob/master/OWNERS)**

Approvers can indicate their approval by writing `/approve` in a comment
Approvers can cancel approval by writing `/approve cancel` in a comment
</details>
<!-- META={"approvers":["x893675"]} -->

Patch:
diff --git a/go.mod b/go.mod
index 0c01093..47b7da5 100644
--- a/go.mod
+++ b/go.mod
@@ -140,6 +140,7 @@ require (
 	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
 	github.com/modern-go/reflect2 v1.0.2 // indirect
 	github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
+	github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
 	github.com/nats-io/jwt/v2 v2.0.3 // indirect
 	github.com/nats-io/nkeys v0.3.0 // indirect
 	github.com/nats-io/nuid v1.0.1 // indirect
diff --git a/go.sum b/go.sum
index 932e5a9..40a4591 100644
--- a/go.sum
+++ b/go.sum
@@ -672,6 +672,7 @@ github.com/munnerz/goautoneg v0.0.0-20120707110453-a547fc61f48d/go.mod h1:+n7T8m
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
 github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
 github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U=
+github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f h1:y5//uYreIhSUg3J1GEMiLbxo1LJaP8RfCpH6pymGZus=
 github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f/go.mod h1:ZdcZmHo+o7JKHSa8/e818NopupXU1YMK5fe1lsApnBw=
 github.com/nats-io/jwt v1.2.2 h1:w3GMTO969dFg+UOKTmmyuu7IGdusK+7Ytlt//OYH/uU=
 github.com/nats-io/jwt v1.2.2/go.mod h1:/xX356yQA6LuXI9xWW7mZNpxgF2mBmGecH+Fj34sP5Q=
diff --git a/pkg/apis/core/v1/handler.go b/pkg/apis/core/v1/handler.go
index 29ebc78..8cba684 100644
--- a/pkg/apis/core/v1/handler.go
+++ b/pkg/apis/core/v1/handler.go
@@ -44,14 +44,11 @@ import (
 	"k8s.io/apimachinery/pkg/util/json"
 	r "k8s.io/apimachinery/pkg/util/rand"
 	"k8s.io/apimachinery/pkg/util/sets"
+	"k8s.io/client-go/tools/clientcmd"
 	"k8s.io/client-go/tools/remotecommand"
 
 	tenantapiv1 "github.com/kubeclipper/kubeclipper/pkg/apis/tenant/v1"
-	tenantv1 "github.com/kubeclipper/kubeclipper/pkg/scheme/tenant/v1"
-
-	"github.com/kubeclipper/kubeclipper/pkg/models/tenant"
-	reqpkg "github.com/kubeclipper/kubeclipper/pkg/server/request"
-
+	"github.com/kubeclipper/kubeclipper/pkg/authentication/auth"
 	"github.com/kubeclipper/kubeclipper/pkg/client/clientrest"
 	"github.com/kubeclipper/kubeclipper/pkg/clustermanage"
 	"github.com/kubeclipper/kubeclipper/pkg/component"
@@ -64,12 +61,16 @@ import (
 	"github.com/kubeclipper/kubeclipper/pkg/models/lease"
 	"github.com/kubeclipper/kubeclipper/pkg/models/operation"
 	"github.com/kubeclipper/kubeclipper/pkg/models/platform"
+	"github.com/kubeclipper/kubeclipper/pkg/models/tenant"
 	"github.com/kubeclipper/kubeclipper/pkg/oplog"
 	"github.com/kubeclipper/kubeclipper/pkg/query"
 	"github.com/kubeclipper/kubeclipper/pkg/scheme/common"
 	v1 "github.com/kubeclipper/kubeclipper/pkg/scheme/core/v1"
 	"github.com/kubeclipper/kubeclipper/pkg/scheme/core/v1/k8s"
 	"github.com/kubeclipper/kubeclipper/pkg/scheme/core/validation"
+	tenantv1 "github.com/kubeclipper/kubeclipper/pkg/scheme/tenant/v1"
+	apirequest "github.com/kubeclipper/kubeclipper/pkg/server/request"
+	reqpkg "github.com/kubeclipper/kubeclipper/pkg/server/request"
 	"github.com/kubeclipper/kubeclipper/pkg/server/restplus"
 	"github.com/kubeclipper/kubeclipper/pkg/service"
 	bs "github.com/kubeclipper/kubeclipper/pkg/simple/backupstore"
@@ -92,6 +93,7 @@ type handler struct {
 	coreOperator     core.Operator
 	delivery         service.IDelivery
 	tenantOperator   tenant.Operator
+	tokenOperator    auth.TokenManagementInterface
 }
 
 const (
@@ -107,7 +109,8 @@ var (
 )
 
 func newHandler(clusterOperator cluster.Operator, op operation.Operator, leaseOperator lease.Operator,
-	platform platform.Operator, coreOperator core.Operator, delivery service.IDelivery, tenantOperator tenant.Operator) *handler {
+	platform platform.Operator, coreOperator core.Operator, delivery service.IDelivery,
+	tokenOperator auth.TokenManagementInterface, tenantOperator tenant.Operator) *handler {
 	return &handler{
 		clusterOperator:  clusterOperator,
 		delivery:         delivery,
@@ -116,6 +119,7 @@ func newHandler(clusterOperator cluster.Operator, op operation.Operator, leaseOp
 		leaseOperator:    leaseOperator,
 		coreOperator:     coreOperator,
 		tenantOperator:   tenantOperator,
+		tokenOperator:    tokenOperator,
 	}
 }
 
@@ -834,6 +838,8 @@ func (h *handler) UpdateClusterCertification(request *restful.Request, response
 
 func (h *handler) GetKubeConfig(request *restful.Request, response *restful.Response) {
 	name := request.PathParameter(query.ParameterName)
+	proxyMode := strings.ToLower(request.QueryParameter("proxy")) == "true"
+
 	ctx := request.Request.Context()
 	clu, err := h.clusterOperator.GetCluster(ctx, name)
 	if err != nil {
@@ -850,23 +856,65 @@ func (h *handler) GetKubeConfig(request *restful.Request, response *restful.Resp
 		}
 	}
 
-	extraMeta, err := h.getClusterMetadata(ctx, clu)
-	if err != nil {
-		restplus.HandleInternalError(response, request, err)
-		return
+	var (
+		kubeConfigData []byte
+	)
+	if proxyMode {
+		kubeConfigData, err = h.getProxyKubeConfig(ctx, name, request.Request.Host)
+		if err != nil {
+			restplus.HandleInternalError(response, request, err)
+			return
+		}
+	} else {
+		extraMeta, err := h.getClusterMetadata(ctx, clu)
+		if err != nil {
+			restplus.HandleInternalError(response, request, err)
+			return
+		}
+
+		if clu.Annotations != nil && clu.Annotations[common.AnnotationActualName] != "" {
+			extraMeta.ClusterName = clu.Annotations[common.AnnotationActualName]
+		}
+
+		kubeConfig, err := k8s.GetKubeConfig(context.TODO(), extraMeta.ClusterName, extraMeta.Masters[0], h.delivery)
+		if err != nil {
+			restplus.HandleInternalError(response, request, err)
+			return
+		}
+		kubeConfigData = []byte(kubeConfig)
 	}
 
-	if clu.Annotations != nil && clu.Annotations[common.AnnotationActualName] != "" {
-		extraMeta.ClusterName = clu.Annotations[common.AnnotationActualName]
+	_, _ = response.Write(kubeConfigData)
+}
+
+func (h *handler) getProxyKubeConfig(ctx context.Context, clusterName string, kcHost string) ([]byte, error) {
+	// FIXME: KC support secure port deploy
+	var serverCA []byte
+	// TODO: get CA from kc server config
+	serverURL := fmt.Sprintf("https://%s/cluster/%s", kcHost, clusterName)
+	user, ok := apirequest.UserFrom(ctx)
+	if !ok {
+		return nil, fmt.Errorf("not get user info")
 	}
 
-	kubeConfig, err := k8s.GetKubeConfig(context.TODO(), extraMeta.ClusterName, extraMeta.Masters[0], h.delivery)
+	token, err := h.tokenOperator.IssueTo(user)
 	if err != nil {
-		restplus.HandleInternalError(response, request, err)
-		return
+		return nil, fmt.Errorf("issue token:%w", err)
+	}
+
+	kubecfg := CreateWithToken(serverURL, clusterName, user.GetName(), serverCA, token.AccessToken)
+	// skip all tls verify
+	for _, c := range kubecfg.Clusters {
+		if strings.HasPrefix(strings.ToLower(c.Server), "https") {
+			c.InsecureSkipTLSVerify = true
+		}
 	}
 
-	_, _ = response.Write([]byte(kubeConfig))
+	buf, err := clientcmd.Write(*kubecfg)
+	if err != nil {
+		return nil, err
+	}
+	return buf, nil
 }
 
 func (h *handler) ListNodes(request *restful.Request, response *restful.Response) {
diff --git a/pkg/apis/core/v1/registry.go b/pkg/apis/core/v1/registry.go
index e6ce832..0f2b498 100644
--- a/pkg/apis/core/v1/registry.go
+++ b/pkg/apis/core/v1/registry.go
@@ -21,6 +21,7 @@ package v1
 import (
 	"net/http"
 
+	"github.com/kubeclipper/kubeclipper/pkg/authentication/auth"
 	"github.com/kubeclipper/kubeclipper/pkg/models/core"
 	"github.com/kubeclipper/kubeclipper/pkg/models/tenant"
 
@@ -258,6 +259,8 @@ func SetupWebService(h *handler) *restful.WebService {
 		Param(webservice.PathParameter(query.ParameterName, "cluster name").
 			Required(true).
 			DataType("string")).
+		Param(webservice.QueryParameter("proxy", "use kubeClipper proxy").
+			Required(false).DataType("boolean").DefaultValue("false")).
 		Returns(http.StatusOK, http.StatusText(http.StatusOK), clientcmdapi.Config{}).
 		Returns(http.StatusNotFound, http.StatusText(http.StatusNotFound), nil))
 	webservice.Route(webservice.GET("/projects/{project}/clusters/{name}/kubeconfig").
@@ -1590,9 +1593,10 @@ func SetupWebService(h *handler) *restful.WebService {
 	return webservice
 }
 
-func AddToContainer(c *restful.Container, clusterOperator cluster.Operator, op operation.Operator, platform platform.Operator,
-	leaseOperator lease.Operator, coreOperator core.Operator, delivery service.IDelivery, tenantOperator tenant.Operator) error {
-	h := newHandler(clusterOperator, op, leaseOperator, platform, coreOperator, delivery, tenantOperator)
+func AddToContainer(c *restful.Container, clusterOperator cluster.Operator, op operation.Operator, platform platform.Operator, leaseOperator lease.Operator, coreOperator core.Operator,
+	delivery service.IDelivery, tokenOperator auth.TokenManagementInterface,
+	tenantOperator tenant.Operator) error {
+	h := newHandler(clusterOperator, op, leaseOperator, platform, coreOperator, delivery, tokenOperator, tenantOperator)
 	webservice := SetupWebService(h)
 	c.Add(webservice)
 	return nil
diff --git a/pkg/apis/core/v1/utils.go b/pkg/apis/core/v1/utils.go
index f0eb111..dfd1059 100644
--- a/pkg/apis/core/v1/utils.go
+++ b/pkg/apis/core/v1/utils.go
@@ -27,6 +27,7 @@ import (
 	"k8s.io/apimachinery/pkg/util/sets"
 
 	"github.com/google/uuid"
+	clientcmdapi "k8s.io/client-go/tools/clientcmd/api"
 
 	"github.com/kubeclipper/kubeclipper/pkg/models/cluster"
 
@@ -402,3 +403,37 @@ func (h *handler) parseAddonStep(ctx context.Context, clu *v1.Cluster, addons []
 	clu.ContainerRuntime.InsecureRegistry = registry.List()
 	return steps, nil
 }
+
+// CreateWithToken creates a KubeConfig object with access to the API server with a token
+// Copy from  k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig/kubeconfig.go
+func CreateWithToken(serverURL, clusterName, userName string, caCert []byte, token string) *clientcmdapi.Config {
+	config := CreateBasic(serverURL, clusterName, userName, caCert)
+	config.AuthInfos[userName] = &clientcmdapi.AuthInfo{
+		Token: token,
+	}
+	return config
+}
+
+// CreateBasic creates a basic, general KubeConfig object that then can be extended
+// Copy from  k8s.io/kubernetes/cmd/kubeadm/app/util/kubeconfig/kubeconfig.go
+func CreateBasic(serverURL, clusterName, userName string, caCert []byte) *clientcmdapi.Config {
+	// Use the cluster and the username as the context name
+	contextName := fmt.Sprintf("%s@%s", userName, clusterName)
+
+	return &clientcmdapi.Config{
+		Clusters: map[string]*clientcmdapi.Cluster{
+			clusterName: {
+				Server:                   serverURL,
+				CertificateAuthorityData: caCert,
+			},
+		},
+		Contexts: map[string]*clientcmdapi.Context{
+			contextName: {
+				Cluster:  clusterName,
+				AuthInfo: userName,
+			},
+		},
+		AuthInfos:      map[string]*clientcmdapi.AuthInfo{},
+		CurrentContext: contextName,
+	}
+}
diff --git a/pkg/apis/iam/v1/handler.go b/pkg/apis/iam/v1/handler.go
index 1300db9..1e6e2f3 100644
--- a/pkg/apis/iam/v1/handler.go
+++ b/pkg/apis/iam/v1/handler.go
@@ -1123,7 +1123,7 @@ func (h *handler) updateRoleBinding(ctx context.Context, operator authuser.Info,
 		ResourceRequest: true,
 		Path:            "",
 	}
-	decision, _, err := h.authz.Authorize(authRecord)
+	decision, _, err := h.authz.Authorize(ctx, authRecord)
 	if err != nil {
 		return err
 	}
@@ -1222,7 +1222,7 @@ func (h *handler) UpdateUserPassword(request *restful.Request, response *restful
 		Path:            "",
 	}
 
-	decision, _, err := h.authz.Authorize(authRecord)
+	decision, _, err := h.authz.Authorize(request.Request.Context(), authRecord)
 	if err != nil {
 		restplus.HandleInternalError(response, request, err)
 		return
diff --git a/pkg/apis/proxy/handler.go b/pkg/apis/proxy/handler.go
new file mode 100644
index 0000000..7002ea0
--- /dev/null
+++ b/pkg/apis/proxy/handler.go
@@ -0,0 +1,171 @@
+/*
+ *
+ *  * Copyright 2021 KubeClipper 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 proxy
+
+import (
+	"fmt"
+	"net"
+	"net/http"
+	"net/url"
+	"strings"
+	"time"
+
+	apimachineryErrors "k8s.io/apimachinery/pkg/api/errors"
+	utilnet "k8s.io/apimachinery/pkg/util/net"
+	"k8s.io/apimachinery/pkg/util/proxy"
+	"k8s.io/client-go/rest"
+	"k8s.io/client-go/tools/clientcmd"
+	"k8s.io/client-go/transport"
+
+	"github.com/kubeclipper/kubeclipper/pkg/models/cluster"
+)
+
+const (
+	ProxyHandlerPrefix = "/cluster/"
+)
+
+type handler struct {
+	clusterReader cluster.ClusterReader
+}
+
+func (h *handler) ServeHTTP(writer http.ResponseWriter, request *http.Request) {
+	clusterName, k8sResource := parseClusterFromPath(request.URL.Path)
+	if clusterName == "" {
+		http.Error(writer, "cluster name cannot be empty", http.StatusBadRequest)
+		return
+	}
+
+	c, err := h.clusterReader.GetClusterEx(request.Context(), clusterName, "0")
+	if err != nil {
+		if apimachineryErrors.IsNotFound(err) {
+			http.Error(writer, fmt.Sprintf("cluster %s not exists", clusterName), http.StatusNotFound)
+		} else {
+			http.Error(writer, err.Error(), http.StatusInternalServerError)
+		}
+		return
+	}
+	if len(c.KubeConfig) == 0 {
+		http.Error(writer, fmt.Sprintf("cluster %s not init", clusterName), http.StatusBadRequest)
+		return
+	}
+
+	restConfig, err := clientcmd.RESTConfigFromKubeConfig(c.KubeConfig)
+	if err != nil {
+		http.Error(writer, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	request.URL.Path = k8sResource
+	request.Header.Del("Authorization")
+
+	proxyHandle, err := newClusterProxyHandler(restConfig, request.URL)
+	if err != nil {
+		http.Error(writer, err.Error(), http.StatusInternalServerError)
+		return
+	}
+	proxyHandle.ServeHTTP(writer, request)
+}
+
+func newHandler(clusterReader cluster.ClusterReader) *handler {
+	return &handler{
+		clusterReader: clusterReader,
+	}
+}
+
+func newClusterProxyHandler(restConfig *rest.Config, requestURL *url.URL) (http.Handler, error) {
+	clusterURL, err := toClusterURL(restConfig.Host, requestURL)
+	if err != nil {
+		return nil, err
+	}
+	restTransport, err := rest.TransportFor(restConfig)
+	if err != nil {
+		return nil, err
+	}
+	upgrade, err := makeUpgradeTransport(restConfig, 0)
+	if err != nil {
+		return nil, err
+	}
+	h := proxy.NewUpgradeAwareHandler(clusterURL, restTransport, true, false, nil)
+	h.UpgradeTransport = upgrade
+	return h, nil
+}
+
+func toClusterURL(restHost string, requestURL *url.URL) (*url.URL, error) {
+	if requestURL == nil {
+		return nil, fmt.Errorf("request url is nil")
+	}
+	if !strings.HasPrefix(strings.ToLower(restHost), "http") {
+		restHost = "http://" + restHost
+	}
+	u, err := url.Parse(restHost)
+	if err != nil {
+		return nil, fmt.Errorf("parse restHost:%w", err)
+	}
+	if requestURL != nil {
+		// without host and scheme
+		u.Opaque = requestURL.Opaque
+		u.User = requestURL.User
+		u.Path = requestURL.Path
+		u.RawPath = requestURL.RawPath
+		u.OmitHost = requestURL.OmitHost
+		u.ForceQuery = requestURL.ForceQuery
+		u.RawQuery = requestURL.RawQuery
+		u.Fragment = requestURL.Fragment
+		u.RawFragment = requestURL.RawFragment
+	}
+	return u, nil
+}
+
+// copy from k8s.io/kubectl/pkg/proxy/proxy_server.go
+// makeUpgradeTransport creates a transport that explicitly bypasses HTTP2 support
+// for proxy connections that must upgrade.
+func makeUpgradeTransport(config *rest.Config, keepalive time.Duration) (proxy.UpgradeRequestRoundTripper, error) {
+	transportConfig, err := config.TransportConfig()
+	if err != nil {
+		return nil, err
+	}
+	tlsConfig, err := transport.TLSConfigFor(transportConfig)
+	if err != nil {
+		return nil, err
+	}
+	rt := utilnet.SetOldTransportDefaults(&http.Transport{
+		TLSClientConfig: tlsConfig,
+		DialContext: (&net.Dialer{
+			Timeout:   30 * time.Second,
+			KeepAlive: keepalive,
+		}).DialContext,
+	})
+
+	upgrader, err := transport.HTTPWrappersForConfig(transportConfig, proxy.MirrorRequest)
+	if err != nil {
+		return nil, err
+	}
+	return proxy.NewUpgradeRequestRoundTripper(rt, upgrader), nil
+}
+
+func parseClusterFromPath(p string) (clusterName, res string) {
+	p = strings.TrimPrefix(p, ProxyHandlerPrefix)
+	items := strings.SplitN(p, "/", 2)
+	switch len(items) {
+	case 2:
+		return items[0], items[1]
+	case 1:
+		return items[0], ""
+	}
+	return "", ""
+}
diff --git a/pkg/apis/proxy/registry.go b/pkg/apis/proxy/registry.go
new file mode 100644
index 0000000..019e7f6
--- /dev/null
+++ b/pkg/apis/proxy/registry.go
@@ -0,0 +1,32 @@
+/*
+ *
+ *  * Copyright 2021 KubeClipper 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 proxy
+
+import (
+	"github.com/emicklei/go-restful"
+
+	"github.com/kubeclipper/kubeclipper/pkg/models/cluster"
+)
+
+func AddToContainer(c *restful.Container, clusterReader cluster.ClusterReader) error {
+	c.HandleWithFilter(ProxyHandlerPrefix, newHandler(clusterReader))
+	restful.DefaultResponseContentType(restful.MIME_JSON)
+	restful.DefaultRequestContentType(restful.MIME_JSON)
+	return nil
+}
diff --git a/pkg/apis/tenant/v1/handler.go b/pkg/apis/tenant/v1/handler.go
index 686281c..a090be7 100644
--- a/pkg/apis/tenant/v1/handler.go
+++ b/pkg/apis/tenant/v1/handler.go
@@ -252,7 +252,7 @@ func (h *handler) listProjects(ctx context.Context, reqUser user.Info, q *query.
 		ResourceScope:   request.GlobalScope,
 	}
 
-	decision, _, err := h.authorizer.Authorize(listWS)
+	decision, _, err := h.authorizer.Authorize(ctx, listWS)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/authorization/authorizer/interface.go b/pkg/authorization/authorizer/interface.go
index 155c8e2..6dad478 100644
--- a/pkg/authorization/authorizer/interface.go
+++ b/pkg/authorization/authorizer/interface.go
@@ -19,6 +19,7 @@
 package authorizer
 
 import (
+	"context"
 	"net/http"
 
 	"k8s.io/apiserver/pkg/authentication/user"
@@ -84,7 +85,7 @@ type Attributes interface {
 }
 
 type Authorizer interface {
-	Authorize(a Attributes) (authorized Decision, reason string, err error)
+	Authorize(ctx context.Context, a Attributes) (authorized Decision, reason string, err error)
 }
 
 type AuthorizerFunc func(a Attributes) (Decision, string, error)
diff --git a/pkg/authorization/rbac/rbac.go b/pkg/authorization/rbac/rbac.go
index e90b0b2..ee240f1 100644
--- a/pkg/authorization/rbac/rbac.go
+++ b/pkg/authorization/rbac/rbac.go
@@ -20,30 +20,22 @@ import (
 	"context"
 	"fmt"
 
-	v1 "github.com/kubeclipper/kubeclipper/pkg/scheme/common"
-	"github.com/kubeclipper/kubeclipper/pkg/server/request"
-
-	v12 "github.com/kubeclipper/kubeclipper/pkg/scheme/iam/v1"
-
 	"github.com/open-policy-agent/opa/rego"
 	"go.uber.org/zap"
-
-	"github.com/kubeclipper/kubeclipper/pkg/logger"
-
+	rbacv1 "k8s.io/api/rbac/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
-
-	"k8s.io/apiserver/pkg/authentication/user"
-
-	"github.com/kubeclipper/kubeclipper/pkg/utils/sliceutil"
-
-	"github.com/kubeclipper/kubeclipper/pkg/query"
-
 	utilerrors "k8s.io/apimachinery/pkg/util/errors"
-
-	rbacv1 "k8s.io/api/rbac/v1"
+	"k8s.io/apiserver/pkg/authentication/user"
 
 	"github.com/kubeclipper/kubeclipper/pkg/authorization/authorizer"
+	"github.com/kubeclipper/kubeclipper/pkg/logger"
+	"github.com/kubeclipper/kubeclipper/pkg/models/cluster"
 	"github.com/kubeclipper/kubeclipper/pkg/models/iam"
+	"github.com/kubeclipper/kubeclipper/pkg/query"
+	"github.com/kubeclipper/kubeclipper/pkg/scheme/common"
+	iamv1 "github.com/kubeclipper/kubeclipper/pkg/scheme/iam/v1"
+	"github.com/kubeclipper/kubeclipper/pkg/server/request"
+	"github.com/kubeclipper/kubeclipper/pkg/utils/sliceutil"
 )
 
 const (
@@ -55,8 +47,8 @@ var (
 	_ authorizer.Authorizer = (*Authorizer)(nil)
 )
 
-func NewAuthorizer(am iam.Operator) authorizer.Authorizer {
-	return &Authorizer{am: am}
+func NewAuthorizer(am iam.Operator, cm cluster.Operator) authorizer.Authorizer {
+	return &Authorizer{am: am, cm: cm}
 }
 
 type authorizingVisitor struct {
@@ -86,12 +78,13 @@ func (v *authorizingVisitor) visit(source fmt.Stringer, regoPolicy string, rule
 
 type Authorizer struct {
 	am iam.Operator
+	cm cluster.Operator
 }
 
-func (a *Authorizer) Authorize(attributes authorizer.Attributes) (authorizer.Decision, string, error) {
+func (a *Authorizer) Authorize(ctx context.Context, attributes authorizer.Attributes) (authorizer.Decision, string, error) {
 	ruleCheckingVisitor := &authorizingVisitor{requestAttributes: attributes}
 
-	a.visitRulesFor(attributes, ruleCheckingVisitor.visit)
+	a.visitRulesFor(ctx, attributes, ruleCheckingVisitor.visit)
 
 	if ruleCheckingVisitor.allowed {
 		return authorizer.DecisionAllow, ruleCheckingVisitor.reason, nil
@@ -104,8 +97,8 @@ func (a *Authorizer) Authorize(attributes authorizer.Attributes) (authorizer.Dec
 	return authorizer.DecisionNoOpinion, reason, nil
 }
 
-func (a *Authorizer) visitRulesFor(requestAttributes authorizer.Attributes, visitor func(source fmt.Stringer, regoPolicy string, rule *rbacv1.PolicyRule, err error) bool) {
-	if globalRoleBindings, err := a.am.ListRoleBindings(context.TODO(), &query.Query{
+func (a *Authorizer) visitRulesFor(ctx context.Context, requestAttributes authorizer.Attributes, visitor func(source fmt.Stringer, regoPolicy string, rule *rbacv1.PolicyRule, err error) bool) {
+	if globalRoleBindings, err := a.am.ListRoleBindings(ctx, &query.Query{
 		Pagination:      query.NoPagination(),
 		ResourceVersion: "0",
 		LabelSelector:   "",
@@ -121,7 +114,7 @@ func (a *Authorizer) visitRulesFor(requestAttributes authorizer.Attributes, visi
 			if !applies {
 				continue
 			}
-			regoPolicy, rules, err := a.getRoleReferenceRules(globalRoleBinding.RoleRef)
+			regoPolicy, rules, err := a.getRoleReferenceRules(ctx, globalRoleBinding.RoleRef)
 			if err != nil {
 				visitor(nil, "", nil, err)
 				continue
@@ -140,11 +133,23 @@ func (a *Authorizer) visitRulesFor(requestAttributes authorizer.Attributes, visi
 	}
 
 	project := requestAttributes.GetProject()
+	if project == "" &&
+		requestAttributes.GetResource() == "cluster" &&
+		requestAttributes.GetSubresource() == "proxy" {
+		c, err := a.cm.GetClusterEx(ctx, requestAttributes.GetName(), "")
+		if err != nil && !visitor(nil, "", nil, err) {
+			return
+		}
+		if c != nil {
+			project = c.Labels[common.LabelProject]
+		}
+	}
+
 	if requestAttributes.GetResourceScope() == request.ProjectScope {
-		if projectRoleBindings, err := a.am.ListProjectRoleBinding(context.TODO(), &query.Query{
+		if projectRoleBindings, err := a.am.ListProjectRoleBinding(ctx, &query.Query{
 			Pagination:      query.NoPagination(),
 			ResourceVersion: "0",
-			LabelSelector:   fmt.Sprintf("%s=%s", v1.LabelProject, project),
+			LabelSelector:   fmt.Sprintf("%s=%s", common.LabelProject, project),
 			FieldSelector:   "",
 		}); err != nil {
 			if !visitor(nil, "", nil, err) {
@@ -157,7 +162,7 @@ func (a *Authorizer) visitRulesFor(requestAttributes authorizer.Attributes, visi
 				if !applies {
 					continue
 				}
-				regoPolicy, rules, err := a.getRoleReferenceRules(projectRoleBinding.RoleRef)
+				regoPolicy, rules, err := a.getRoleReferenceRules(ctx, projectRoleBinding.RoleRef)
 				if err != nil {
 					visitor(nil, "", nil, err)
 					continue
@@ -178,27 +183,27 @@ func (a *Authorizer) visitRulesFor(requestAttributes authorizer.Attributes, visi
 }
 
 // TODO: add am interface rather than iam.Operator
-func (a *Authorizer) getRoleReferenceRules(roleRef rbacv1.RoleRef) (regoPolicy string, rules []rbacv1.PolicyRule, err error) {
+func (a *Authorizer) getRoleReferenceRules(ctx context.Context, roleRef rbacv1.RoleRef) (regoPolicy string, rules []rbacv1.PolicyRule, err error) {
 	empty := make([]rbacv1.PolicyRule, 0)
 	switch roleRef.Kind {
-	case v1.ResourceKindGlobalRole:
-		globalRole, err := a.am.GetRoleEx(context.TODO(), roleRef.Name, "0")
+	case common.ResourceKindGlobalRole:
+		globalRole, err := a.am.GetRoleEx(ctx, roleRef.Name, "0")
 		if err != nil {
 			if errors.IsNotFound(err) {
 				return "", empty, nil
 			}
 			return "", nil, err
 		}
-		return globalRole.Annotations[v1.RegoOverrideAnnotation], globalRole.Rules, nil
-	case v1.ResourceKindProjectGlobalRole:
-		projectRole, err := a.am.GetProjectRoleEx(context.TODO(), roleRef.Name, "0")
+		return globalRole.Annotations[common.RegoOverrideAnnotation], globalRole.Rules, nil
+	case common.ResourceKindProjectGlobalRole:
+		projectRole, err := a.am.GetProjectRoleEx(ctx, roleRef.Name, "0")
 		if err != nil {
 			if errors.IsNotFound(err) {
 				return "", empty, nil
 			}
 			return "", nil, err
 		}
-		return projectRole.Annotations[v1.RegoOverrideAnnotation], projectRole.Rules, nil
+		return projectRole.Annotations[common.RegoOverrideAnnotation], projectRole.Rules, nil
 
 	default:
 		return "", nil, fmt.Errorf("unsupported role reference kind: %q", roleRef.Kind)
@@ -206,7 +211,7 @@ func (a *Authorizer) getRoleReferenceRules(roleRef rbacv1.RoleRef) (regoPolicy s
 }
 
 type globalRoleBindingDescriber struct {
-	binding *v12.GlobalRoleBinding
+	binding *iamv1.GlobalRoleBinding
 	subject *rbacv1.Subject
 }
 
@@ -220,7 +225,7 @@ func (d *globalRoleBindingDescriber) String() string {
 }
 
 type projectRoleBindingDescriber struct {
-	binding *v12.ProjectRoleBinding
+	binding *iamv1.ProjectRoleBinding
 	subject *rbacv1.Subject
 }
 
diff --git a/pkg/component/common/image.go b/pkg/component/common/image.go
new file mode 100644
index 0000000..8805938
--- /dev/null
+++ b/pkg/component/common/image.go
@@ -0,0 +1,118 @@
+package common
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"runtime"
+	"time"
+
+	"github.com/kubeclipper/kubeclipper/pkg/component"
+	"github.com/kubeclipper/kubeclipper/pkg/component/utils"
+	"github.com/kubeclipper/kubeclipper/pkg/logger"
+	v1 "github.com/kubeclipper/kubeclipper/pkg/scheme/core/v1"
+	"github.com/kubeclipper/kubeclipper/pkg/simple/downloader"
+	"github.com/kubeclipper/kubeclipper/pkg/utils/strutil"
+	"go.uber.org/zap"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+)
+
+const (
+	imageName  = "image"
+	AgentImage = "AgentImager"
+)
+
+func init() {
+	if err := component.RegisterAgentStep(fmt.Sprintf(component.RegisterStepKeyFormat, imageName, version, AgentImage), &Imager{}); err != nil {
+		panic(err)
+	}
+}
+
+type Imager struct {
+	PkgName string `json:"pkgName"`
+	Version string `json:"version"`
+	Offline bool   `json:"offline"`
+	CriName string `json:"criName"`
+	// Optional. If the value of the change field is not empty, the DownloadCustomImages and RemoveCustomImages operations will be performed
+	CustomImageList []string `json:"customConfig"`
+}
+
+func (i *Imager) Install(ctx context.Context, opts component.Options) ([]byte, error) {
+	instance, err := downloader.NewInstance(ctx, i.PkgName, i.Version, runtime.GOARCH, !i.Offline, opts.DryRun)
+	if err != nil {
+		return nil, err
+	}
+
+	var dstFiles []string
+
+	if len(i.CustomImageList) > 0 {
+		dstFiles, err = instance.DownloadCustomImages(i.CustomImageList...)
+		if err != nil {
+			return nil, fmt.Errorf("download %s-%s custom image failed: %v", i.PkgName, i.Version, err)
+		}
+	} else {
+		dstFile, err := instance.DownloadImages()
+		if err != nil {
+			return nil, fmt.Errorf("download %s-%s image failed: %v", i.PkgName, i.Version, err)
+		}
+		dstFiles = []string{dstFile}
+	}
+
+	// load image package
+	for _, dstFile := range dstFiles {
+		if err := utils.LoadImage(ctx, opts.DryRun, dstFile, i.CriName); err != nil {
+			return nil, fmt.Errorf("load %s-%s image failed: %v", i.PkgName, i.Version, err)
+		}
+	}
+
+	logger.Infof("%s-%s image packages offline install successfully", i.PkgName, i.Version)
+	return nil, err
+}
+
+func (i *Imager) Uninstall(ctx context.Context, opts component.Options) ([]byte, error) {
+	instance, err := downloader.NewInstance(ctx, imageName, i.Version, runtime.GOARCH, !i.Offline, opts.DryRun)
+	if err != nil {
+		return nil, err
+	}
+
+	if len(i.CustomImageList) > 0 {
+		if err = instance.RemoveCustomImages(i.CustomImageList...); err != nil {
+			logger.Errorf("remove %s-%s custom-images compressed file failed", i.PkgName, i.Version, zap.Error(err))
+		}
+	} else {
+		if err = instance.RemoveImages(); err != nil {
+			logger.Errorf("remove %s-%s images compressed file failed", i.PkgName, i.Version, zap.Error(err))
+		}
+	}
+
+	return nil, nil
+}
+
+func (i *Imager) NewInstance() component.ObjectMeta {
+	return &Imager{}
+}
+
+func (i *Imager) InstallSteps(nodeList component.NodeList) ([]v1.Step, error) {
+	customCommand, err := json.Marshal(i)
+	if err != nil {
+		return nil, err
+	}
+	return []v1.Step{
+		{
+			ID:         strutil.GetUUID(),
+			Name:       fmt.Sprintf("%s-imageLoad", i.PkgName),
+			Timeout:    metav1.Duration{Duration: 30 * time.Minute},
+			ErrIgnore:  false,
+			RetryTimes: 0,
+			Nodes:      utils.UnwrapNodeList(nodeList),
+			Action:     v1.ActionInstall,
+			Commands: []v1.Command{
+				{
+					Type:          v1.CommandCustom,
+					Identity:      fmt.Sprintf(component.RegisterTemplateKeyFormat, imageName, version, AgentImage),
+					CustomCommand: customCommand,
+				},
+			},
+		},
+	}, nil
+}
diff --git a/pkg/component/nfs/nfs.go b/pkg/component/nfs/nfs.go
index 840abaf..5ba280f 100644
--- a/pkg/component/nfs/nfs.go
+++ b/pkg/component/nfs/nfs.go
@@ -26,13 +26,10 @@ import (
 	"io"
 	"os"
 	"path/filepath"
-	"runtime"
 	"time"
 
 	"github.com/kubeclipper/kubeclipper/pkg/component/common"
 
-	"go.uber.org/zap"
-
 	"github.com/nicksnyder/go-i18n/v2/i18n"
 
 	"github.com/kubeclipper/kubeclipper/pkg/utils/strutil"
@@ -42,9 +39,7 @@ import (
 	"github.com/kubeclipper/kubeclipper/pkg/component"
 	"github.com/kubeclipper/kubeclipper/pkg/component/utils"
 	"github.com/kubeclipper/kubeclipper/pkg/component/validation"
-	"github.com/kubeclipper/kubeclipper/pkg/logger"
 	v1 "github.com/kubeclipper/kubeclipper/pkg/scheme/core/v1"
-	"github.com/kubeclipper/kubeclipper/pkg/simple/downloader"
 	"github.com/kubeclipper/kubeclipper/pkg/utils/fileutil"
 	"github.com/kubeclipper/kubeclipper/pkg/utils/netutil"
 	tmplutil "github.com/kubeclipper/kubeclipper/pkg/utils/template"
@@ -60,9 +55,6 @@ func init() {
 		panic(err)
 	}
 
-	if err := component.RegisterAgentStep(fmt.Sprintf(component.RegisterTemplateKeyFormat, name, version, AgentImageLoader), &ImageLoader{}); err != nil {
-		panic(err)
-	}
 	if err := initI18nForComponentMeta(); err != nil {
 		panic(err)
 	}
@@ -71,7 +63,6 @@ func init() {
 var (
 	_ component.Interface      = (*NFSProvisioner)(nil)
 	_ component.TemplateRender = (*NFSProvisioner)(nil)
-	_ component.StepRunnable   = (*ImageLoader)(nil)
 )
 
 const (
@@ -175,31 +166,17 @@ func (n *NFSProvisioner) InitSteps(ctx context.Context) error {
 	}
 	if metadata.Offline && n.ImageRepoMirror == "" {
 		// TODO: arch is unnecessary, version can be configured
-		imageloader := &ImageLoader{
+		imager := &common.Imager{
+			PkgName: nfs,
 			Version: "v4.0.2",
-			CriType: metadata.CRI,
+			CriName: metadata.CRI,
 			Offline: metadata.Offline,
 		}
-		iData, err := json.Marshal(imageloader)
+		steps, err := imager.InstallSteps(metadata.GetAllNodes())
 		if err != nil {
 			return err
 		}
-		n.installSteps = append(n.installSteps, v1.Step{
-			ID:         strutil.GetUUID(),
-			Name:       "imageLoader",
-			Timeout:    metav1.Duration{Duration: 5 * time.Minute},
-			ErrIgnore:  false,
-			RetryTimes: 1,
-			Nodes:      utils.UnwrapNodeList(metadata.GetAllNodes()),
-			Action:     v1.ActionInstall,
-			Commands: []v1.Command{
-				{
-					Type:          v1.CommandCustom,
-					Identity:      fmt.Sprintf(component.RegisterTemplateKeyFormat, name, version, AgentImageLoader),
-					CustomCommand: iData,
-				},
-			},
-		})
+		n.installSteps = append(n.installSteps, steps...)
 	}
 
 	bytes, err := json.Marshal(n)
@@ -479,41 +456,3 @@ func (n *NFSProvisioner) Render(ctx context.Context, opts component.Options) err
 func (n *NFSProvisioner) GetImageRepoMirror() string {
 	return n.ImageRepoMirror
 }
-
-type ImageLoader struct {
-	Version string
-	CriType string
-	Offline bool
-}
-
-func (n *ImageLoader) Install(ctx context.Context, opts component.Options) ([]byte, error) {
-	instance, err := downloader.NewInstance(ctx, nfs, n.Version, runtime.GOARCH, !n.Offline, opts.DryRun)
-	if err != nil {
-		return nil, err
-	}
-	dstFile, err := instance.DownloadImages()
-	if err != nil {
-		return nil, err
-	}
-	// load image package
-	if err = utils.LoadImage(ctx, opts.DryRun, dstFile, n.CriType); err == nil {
-		logger.Info("nfs packages offline install successfully")
-	}
-
-	return nil, err
-}
-
-func (n *ImageLoader) Uninstall(ctx context.Context, opts component.Options) ([]byte, error) {
-	instance, err := downloader.NewInstance(ctx, nfs, n.Version, runtime.GOARCH, !n.Offline, opts.DryRun)
-	if err != nil {
-		return nil, err
-	}
-	if err = instance.RemoveImages(); err != nil {
-		logger.Error("remove nfs images compressed file failed", zap.Error(err))
-	}
-	return nil, nil
-}
-
-func (n *ImageLoader) NewInstance() component.ObjectMeta {
-	return &ImageLoader{}
-}
diff --git a/pkg/component/nfscsi/nfs_csi.go b/pkg/component/nfscsi/nfs_csi.go
index e2651f0..b384294 100644
--- a/pkg/component/nfscsi/nfs_csi.go
+++ b/pkg/component/nfscsi/nfs_csi.go
@@ -8,21 +8,16 @@ import (
 	"io"
 	"os"
 	"path/filepath"
-	"runtime"
 	"time"
 
 	"github.com/nicksnyder/go-i18n/v2/i18n"
-	"go.uber.org/zap"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 
 	"github.com/kubeclipper/kubeclipper/pkg/component"
 	"github.com/kubeclipper/kubeclipper/pkg/component/common"
-	nfsprovisioner "github.com/kubeclipper/kubeclipper/pkg/component/nfs"
 	"github.com/kubeclipper/kubeclipper/pkg/component/utils"
 	"github.com/kubeclipper/kubeclipper/pkg/component/validation"
-	"github.com/kubeclipper/kubeclipper/pkg/logger"
 	v1 "github.com/kubeclipper/kubeclipper/pkg/scheme/core/v1"
-	"github.com/kubeclipper/kubeclipper/pkg/simple/downloader"
 	"github.com/kubeclipper/kubeclipper/pkg/utils/fileutil"
 	"github.com/kubeclipper/kubeclipper/pkg/utils/netutil"
 	"github.com/kubeclipper/kubeclipper/pkg/utils/strutil"
@@ -58,9 +53,6 @@ func init() {
 		panic(err)
 	}
 
-	if err := component.RegisterAgentStep(fmt.Sprintf(component.RegisterTemplateKeyFormat, name, version, AgentImageLoader), &nfsprovisioner.ImageLoader{}); err != nil {
-		panic(err)
-	}
 	if err := initI18nForComponentMeta(); err != nil {
 		panic(err)
 	}
@@ -280,31 +272,17 @@ func (n *NFS) InitSteps(ctx context.Context) error {
 	}
 	if metadata.Offline && n.ImageRepoMirror == "" {
 		// TODO: arch is unnecessary, version can be configured
-		imageloader := &ImageLoader{
+		imager := &common.Imager{
+			PkgName: nfs,
 			Version: "v4.1.0",
-			CriType: metadata.CRI,
+			CriName: metadata.CRI,
 			Offline: metadata.Offline,
 		}
-		iData, err := json.Marshal(imageloader)
+		steps, err := imager.InstallSteps(metadata.GetAllNodes())
 		if err != nil {
 			return err
 		}
-		n.installSteps = append(n.installSteps, v1.Step{
-			ID:         strutil.GetUUID(),
-			Name:       "imageLoader",
-			Timeout:    metav1.Duration{Duration: 5 * time.Minute},
-			ErrIgnore:  false,
-			RetryTimes: 1,
-			Nodes:      utils.UnwrapNodeList(metadata.GetAllNodes()),
-			Action:     v1.ActionInstall,
-			Commands: []v1.Command{
-				{
-					Type:          v1.CommandCustom,
-					Identity:      fmt.Sprintf(component.RegisterTemplateKeyFormat, name, version, AgentImageLoader),
-					CustomCommand: iData,
-				},
-			},
-		})
+		n.installSteps = append(n.installSteps, steps...)
 	}
 	bytes, err := json.Marshal(n)
 	if err != nil {
@@ -405,44 +383,6 @@ func (n *NFS) GetImageRepoMirror() string {
 	return n.ImageRepoMirror
 }
 
-type ImageLoader struct {
-	Version string
-	CriType string
-	Offline bool
-}
-
-func (n *ImageLoader) Install(ctx context.Context, opts component.Options) ([]byte, error) {
-	instance, err := downloader.NewInstance(ctx, nfs, n.Version, runtime.GOARCH, !n.Offline, opts.DryRun)
-	if err != nil {
-		return nil, err
-	}
-	dstFile, err := instance.DownloadImages()
-	if err != nil {
-		return nil, err
-	}
-	// load image package
-	if err = utils.LoadImage(ctx, opts.DryRun, dstFile, n.CriType); err == nil {
-		logger.Info("nfs packages offline install successfully")
-	}
-
-	return nil, err
-}
-
-func (n *ImageLoader) Uninstall(ctx context.Context, opts component.Options) ([]byte, error) {
-	instance, err := downloader.NewInstance(ctx, nfs, n.Version, runtime.GOARCH, !n.Offline, opts.DryRun)
-	if err != nil {
-		return nil, err
-	}
-	if err = instance.RemoveImages(); err != nil {
-		logger.Error("remove nfs images compressed file failed", zap.Error(err))
-	}
-	return nil, nil
-}
-
-func (n *ImageLoader) NewInstance() component.ObjectMeta {
-	return &ImageLoader{}
-}
-
 func initI18nForComponentMeta() error {
 	return component.AddI18nMessages(component.I18nMessages{
 		{
diff --git a/pkg/scheme/core/v1/k8s/node.go b/pkg/scheme/core/v1/k8s/node.go
index 2b0cfb6..018fc57 100644
--- a/pkg/scheme/core/v1/k8s/node.go
+++ b/pkg/scheme/core/v1/k8s/node.go
@@ -167,7 +167,7 @@ func (stepper *GenNode) MakeInstallSteps(metadata *component.ExtraMetadata, patc
 			stepList := newComp.GetInstallSteps()
 			for _, st := range stepList {
 				// TODO: Temporarily use hardcode to adjust, and subsequently optimize the step code for image download and load
-				if st.Name == "imageLoader" {
+				if strings.Contains(st.Name, "imageLoad") {
 					st.Nodes = patchNodes
 					stepper.installSteps = append(stepper.installSteps, st)
 					break
diff --git a/pkg/server/config.go b/pkg/server/config.go
index 78eb14b..62e459f 100644
--- a/pkg/server/config.go
+++ b/pkg/server/config.go
@@ -241,6 +241,11 @@ var Roles = []iamv1.GlobalRole{
 				Resources: []string{"clusters/terminal"},
 				Verbs:     []string{"get"},
 			},
+			{
+				APIGroups: []string{"core.kubeclipper.io"},
+				Resources: []string{"clusters/proxy"},
+				Verbs:     []string{"*"},
+			},
 		},
 	},
 	{
@@ -501,6 +506,11 @@ var Roles = []iamv1.GlobalRole{
 				Resources: []string{"templates"},
 				Verbs:     []string{"*"},
 			},
+			{
+				APIGroups: []string{"core.kubeclipper.io"},
+				Resources: []string{"clusters/proxy"},
+				Verbs:     []string{"*"},
+			},
 		},
 	},
 	{
@@ -1412,6 +1422,11 @@ var ProjectRolesTemplate = []iamv1.ProjectRole{
 				Resources: []string{"clusters/terminal"},
 				Verbs:     []string{"get"},
 			},
+			{
+				APIGroups: []string{"core.kubeclipper.io"},
+				Resources: []string{"clusters/proxy"},
+				Verbs:     []string{"*"},
+			},
 		},
 	},
 	{
@@ -1519,6 +1534,11 @@ var ProjectRolesTemplate = []iamv1.ProjectRole{
 				Resources: []string{"templates"},
 				Verbs:     []string{"*"},
 			},
+			{
+				APIGroups: []string{"core.kubeclipper.io"},
+				Resources: []string{"clusters/proxy"},
+				Verbs:     []string{"*"},
+			},
 		},
 	},
 	{
diff --git a/pkg/server/filters/authorization.go b/pkg/server/filters/authorization.go
index b400130..bbd8767 100644
--- a/pkg/server/filters/authorization.go
+++ b/pkg/server/filters/authorization.go
@@ -44,7 +44,7 @@ func WithAuthorization(authorizers authorizer.Authorizer) restful.FilterFunction
 			restplus.HandleInternalError(response, req, err)
 			return
 		}
-		authorized, reason, err := authorizers.Authorize(attributes)
+		authorized, reason, err := authorizers.Authorize(ctx, attributes)
 		if authorized == authorizer.DecisionAllow {
 			chain.ProcessFilter(req, response)
 			return
diff --git a/pkg/server/request/request.go b/pkg/server/request/request.go
index 8c0330e..d79e297 100644
--- a/pkg/server/request/request.go
+++ b/pkg/server/request/request.go
@@ -81,6 +81,10 @@ eg: /api/core.kubeclipper.io/v1/nodes/node1
 /api/{api-group}/{version}/projects/{project}/{resource}/{resourceName}
 eg: /api/core.kubeclipper.io/v1/projects/demo1/nodes/node1
 
+Kubernetes proxy paths
+/cluster/{cluster-name}/{k8s-resource}
+eg: /cluster/test-cluster/api/v1/namespaces
+
 NonResource paths
 /healthz
 /metrics
@@ -106,13 +110,6 @@ func (i *InfoFactory) NewRequestInfo(req *http.Request) (*Info, error) {
 		return &requestInfo, nil
 	}
 
-	requestInfo.APIPrefix = currentParts[0]
-	requestInfo.APIGroup = currentParts[1]
-	requestInfo.APIVersion = currentParts[2]
-	requestInfo.IsResourceRequest = true
-
-	currentParts = currentParts[3:]
-
 	switch req.Method {
 	case "POST":
 		requestInfo.Verb = "create"
@@ -128,6 +125,22 @@ func (i *InfoFactory) NewRequestInfo(req *http.Request) (*Info, error) {
 		requestInfo.Verb = ""
 	}
 
+	requestInfo.APIPrefix = currentParts[0]
+	switch requestInfo.APIPrefix {
+	case "api":
+		requestInfo.APIGroup = currentParts[1]
+		requestInfo.APIVersion = currentParts[2]
+		requestInfo.IsResourceRequest = true
+		currentParts = currentParts[3:]
+	case "cluster":
+		requestInfo.Name = currentParts[1]
+		requestInfo.APIGroup = "core.kubeclipper.io"
+		requestInfo.IsResourceRequest = true
+		requestInfo.Resource = "clusters"
+		requestInfo.Subresource = "proxy"
+		requestInfo.ResourceScope = ProjectScope
+		return &requestInfo, nil
+	}
 	// parsing successful, so we now know the proper value for .Parts
 	// requestInfo.Parts = currentParts
 	// URL forms: /projects/{project}/*
diff --git a/pkg/server/request/response.go b/pkg/server/request/response.go
new file mode 100644
index 0000000..725b8fc
--- /dev/null
+++ b/pkg/server/request/response.go
@@ -0,0 +1,1 @@
+package request
diff --git a/pkg/server/server.go b/pkg/server/server.go
index 0b874c1..0aa6e5e 100644
--- a/pkg/server/server.go
+++ b/pkg/server/server.go
@@ -25,67 +25,57 @@ import (
 	"net/http"
 	"time"
 
-	"k8s.io/apimachinery/pkg/runtime/schema"
-
-	tenantv1 "github.com/kubeclipper/kubeclipper/pkg/apis/tenant/v1"
-	"github.com/kubeclipper/kubeclipper/pkg/controller/cloudprovidercontroller"
-	"github.com/kubeclipper/kubeclipper/pkg/controller/projectcontroller"
-	"github.com/kubeclipper/kubeclipper/pkg/models/core"
-	"github.com/kubeclipper/kubeclipper/pkg/models/tenant"
-
-	"github.com/kubeclipper/kubeclipper/pkg/controller/cronbackupcontroller"
-
-	corev1 "github.com/kubeclipper/kubeclipper/pkg/apis/core/v1"
-	"github.com/kubeclipper/kubeclipper/pkg/authentication/mfa"
-	"github.com/kubeclipper/kubeclipper/pkg/controller/tokencontroller"
-	"github.com/kubeclipper/kubeclipper/pkg/simple/client/cache"
-
+	"github.com/emicklei/go-restful"
 	"github.com/google/uuid"
-
-	"github.com/kubeclipper/kubeclipper/pkg/controller/regioncontroller"
-
-	"github.com/kubeclipper/kubeclipper/pkg/controller"
-	"github.com/kubeclipper/kubeclipper/pkg/controller-runtime/manager"
-	"github.com/kubeclipper/kubeclipper/pkg/controller/backupcontroller"
-	"github.com/kubeclipper/kubeclipper/pkg/controller/clustercontroller"
-	"github.com/kubeclipper/kubeclipper/pkg/controller/dnscontroller"
-	"github.com/kubeclipper/kubeclipper/pkg/controller/nodecontroller"
-	"github.com/kubeclipper/kubeclipper/pkg/controller/operationcontroller"
-
-	v1 "github.com/kubeclipper/kubeclipper/pkg/scheme/iam/v1"
-	schemetenantv1 "github.com/kubeclipper/kubeclipper/pkg/scheme/tenant/v1"
-
-	"github.com/kubeclipper/kubeclipper/pkg/authentication/request/internaltoken"
-
+	"go.uber.org/zap"
+	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/apimachinery/pkg/util/sets"
 	unionauth "k8s.io/apiserver/pkg/authentication/request/union"
 	etcdRESTOptions "k8s.io/apiserver/pkg/server/options"
 	"k8s.io/component-base/version"
 
-	"github.com/emicklei/go-restful"
-	"go.uber.org/zap"
-
 	auditingv1 "github.com/kubeclipper/kubeclipper/pkg/apis/auditing/v1"
 	configv1 "github.com/kubeclipper/kubeclipper/pkg/apis/config/v1"
+	corev1 "github.com/kubeclipper/kubeclipper/pkg/apis/core/v1"
 	iamv1 "github.com/kubeclipper/kubeclipper/pkg/apis/iam/v1"
 	"github.com/kubeclipper/kubeclipper/pkg/apis/oauth"
+	"github.com/kubeclipper/kubeclipper/pkg/apis/proxy"
+	tenantv1 "github.com/kubeclipper/kubeclipper/pkg/apis/tenant/v1"
 	"github.com/kubeclipper/kubeclipper/pkg/auditing"
 	"github.com/kubeclipper/kubeclipper/pkg/authentication/auth"
+	"github.com/kubeclipper/kubeclipper/pkg/authentication/mfa"
 	"github.com/kubeclipper/kubeclipper/pkg/authentication/request/anonymous"
 	"github.com/kubeclipper/kubeclipper/pkg/authentication/request/bearertoken"
+	"github.com/kubeclipper/kubeclipper/pkg/authentication/request/internaltoken"
 	authnpath "github.com/kubeclipper/kubeclipper/pkg/authentication/request/path"
 	"github.com/kubeclipper/kubeclipper/pkg/authentication/request/wstoken"
 	"github.com/kubeclipper/kubeclipper/pkg/authorization/authorizer"
 	"github.com/kubeclipper/kubeclipper/pkg/authorization/rbac"
 	"github.com/kubeclipper/kubeclipper/pkg/client/informers"
+	"github.com/kubeclipper/kubeclipper/pkg/controller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller-runtime/manager"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/backupcontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/cloudprovidercontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/clustercontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/cronbackupcontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/dnscontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/nodecontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/operationcontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/projectcontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/regioncontroller"
+	"github.com/kubeclipper/kubeclipper/pkg/controller/tokencontroller"
 	"github.com/kubeclipper/kubeclipper/pkg/healthz"
 	"github.com/kubeclipper/kubeclipper/pkg/logger"
 	"github.com/kubeclipper/kubeclipper/pkg/models/cluster"
+	"github.com/kubeclipper/kubeclipper/pkg/models/core"
 	"github.com/kubeclipper/kubeclipper/pkg/models/iam"
 	"github.com/kubeclipper/kubeclipper/pkg/models/lease"
 	"github.com/kubeclipper/kubeclipper/pkg/models/operation"
 	"github.com/kubeclipper/kubeclipper/pkg/models/platform"
+	"github.com/kubeclipper/kubeclipper/pkg/models/tenant"
 	"github.com/kubeclipper/kubeclipper/pkg/query"
+	v1 "github.com/kubeclipper/kubeclipper/pkg/scheme/iam/v1"
+	schemetenantv1 "github.com/kubeclipper/kubeclipper/pkg/scheme/tenant/v1"
 	"github.com/kubeclipper/kubeclipper/pkg/server/config"
 	"github.com/kubeclipper/kubeclipper/pkg/server/filters"
 	"github.com/kubeclipper/kubeclipper/pkg/server/registry"
@@ -93,6 +83,7 @@ import (
 	"github.com/kubeclipper/kubeclipper/pkg/service"
 	"github.com/kubeclipper/kubeclipper/pkg/service/delivery"
 	"github.com/kubeclipper/kubeclipper/pkg/service/staticresource"
+	"github.com/kubeclipper/kubeclipper/pkg/simple/client/cache"
 	"github.com/kubeclipper/kubeclipper/pkg/utils/hashutil"
 	"github.com/kubeclipper/kubeclipper/pkg/utils/metrics"
 )
@@ -194,7 +185,7 @@ func (s *APIServer) Run(stopCh <-chan struct{}) (err error) {
 
 func (s *APIServer) buildHandlerChain(stopCh <-chan struct{}) error {
 	infoFactory := &request.InfoFactory{
-		APIPrefixes: sets.NewString("api"),
+		APIPrefixes: sets.NewString("api", "cluster"),
 		GlobalResources: []schema.GroupResource{
 			schemetenantv1.Resource("projects"),
 		},
@@ -267,7 +258,7 @@ func (s *APIServer) installAPIs(stopCh <-chan struct{}) error {
 	opOperator := operation.NewOperationOperator(s.storageFactory.Operations())
 	iamOperator := iam.NewOperator(s.storageFactory.Users(), s.storageFactory.GlobalRoles(),
 		s.storageFactory.GlobalRoleBindings(), s.storageFactory.Tokens(), s.storageFactory.LoginRecords(), s.storageFactory.ProjectRole(), s.storageFactory.ProjectRoleBinding())
-	s.rbacAuthorizer = rbac.NewAuthorizer(iamOperator)
+	s.rbacAuthorizer = rbac.NewAuthorizer(iamOperator, clusterOperator)
 
 	deliverySvc := delivery.NewService(s.Config.MQOptions, clusterOperator, leaseOperator, opOperator)
 	s.Services = append(s.Services, deliverySvc)
@@ -313,7 +304,11 @@ func (s *APIServer) installAPIs(stopCh <-chan struct{}) error {
 		return err
 	}
 	s.Services = append(s.Services, ctrl)
-	if err = corev1.AddToContainer(s.container, clusterOperator, opOperator, platformOperator, leaseOperator, coreOperator, deliverySvc, tenantOperator); err != nil {
+
+	if err = corev1.AddToContainer(s.container, clusterOperator, opOperator, platformOperator, leaseOperator, coreOperator, deliverySvc, tokenOperator, tenantOperator); err != nil {
+		return err
+	}
+	if err = proxy.AddToContainer(s.container, clusterOperator); err != nil {
 		return err
 	}
 	staticResourceSvc, err := staticresource.NewService(s.Config.StaticServerOptions)
@@ -321,7 +316,6 @@ func (s *APIServer) installAPIs(stopCh <-chan struct{}) error {
 		return err
 	}
 	s.Services = append(s.Services, staticResourceSvc)
-
 	return nil
 }
 
diff --git a/pkg/simple/downloader/downloader.go b/pkg/simple/downloader/downloader.go
index 8661e3f..ced52e5 100644
--- a/pkg/simple/downloader/downloader.go
+++ b/pkg/simple/downloader/downloader.go
@@ -134,6 +134,26 @@ func (dl *Downloader) RemoveImages() error {
 	return os.RemoveAll(filepath.Join(dl.dstDir, ImageFilename))
 }
 
+// DownloadCustomImages download custom image file
+func (dl *Downloader) DownloadCustomImages(imageList ...string) (files []string, err error) {
+	for _, image := range imageList {
+		if err = dl.Download(image); err != nil {
+			return
+		}
+		files = append(files, filepath.Join(dl.dstDir, image))
+	}
+	return
+}
+
+// RemoveCustomImages remove custom image file
+func (dl *Downloader) RemoveCustomImages(imageList ...string) (err error) {
+	for _, v := range imageList {
+		// only the last error is recorded, can ignore it
+		err = os.RemoveAll(v)
+	}
+	return
+}
+
 // DownloadAll download config and image file
 func (dl *Downloader) DownloadAll() (cPath, iPath string, err error) {
 	cPath, err = dl.DownloadAndUnpackConfigs()
diff --git a/tools/doc-gen/main.go b/tools/doc-gen/main.go
index d2d0a5d..a62c265 100644
--- a/tools/doc-gen/main.go
+++ b/tools/doc-gen/main.go
@@ -27,6 +27,7 @@ import (
 	"os"
 
 	auditingv1 "github.com/kubeclipper/kubeclipper/pkg/apis/auditing/v1"
+	"github.com/kubeclipper/kubeclipper/pkg/apis/proxy"
 
 	"github.com/kubeclipper/kubeclipper/pkg/apis/oauth"
 
@@ -99,11 +100,12 @@ func validateSpec(apiSpec []byte) error {
 func generateSwaggerJSON() []byte {
 
 	container := restful.NewContainer()
-	urlruntime.Must(corev1.AddToContainer(container, nil, nil, nil, nil, nil, nil, nil))
+	urlruntime.Must(corev1.AddToContainer(container, nil, nil, nil, nil, nil, nil, nil, nil))
 	urlruntime.Must(iamv1.AddToContainer(container, nil, nil, nil, nil))
 	urlruntime.Must(tenantv1.AddToContainer(container, nil, nil, nil, nil))
 	urlruntime.Must(configv1.AddToContainer(container, nil, nil))
 	urlruntime.Must(oauth.AddToContainer(container, nil, nil, nil, nil, nil, nil, nil))
+	urlruntime.Must(proxy.AddToContainer(container, nil))
 	urlruntime.Must(auditingv1.AddToContainer(container, nil))
 
 	config := restfulspec.Config{


Test patch:
diff --git a/pkg/apis/core/v1/utils_test.go b/pkg/apis/core/v1/utils_test.go
index 5b04ab2..33c7e2d 100644
--- a/pkg/apis/core/v1/utils_test.go
+++ b/pkg/apis/core/v1/utils_test.go
@@ -144,7 +144,7 @@ var (
 )
 
 func Test_parseOperationFromCluster(t *testing.T) {
-	h := newHandler(nil, nil, nil, nil, nil, nil, nil)
+	h := newHandler(nil, nil, nil, nil, nil, nil, nil, nil)
 	type args struct {
 		c      *v1.Cluster
 		meta   *component.ExtraMetadata
@@ -192,7 +192,7 @@ func Test_parseOperationFromComponent(t *testing.T) {
 		cluster    *v1.Cluster
 		components []v1.Addon
 	}
-	h := newHandler(nil, nil, nil, nil, nil, nil, nil)
+	h := newHandler(nil, nil, nil, nil, nil, nil, nil, nil)
 	nfs := nfsprovisioner.NFSProvisioner{
 		ManifestsDir:     "/tmp/.nfs",
 		Namespace:        "kube-system",
diff --git a/pkg/authorization/rbac/rbac_test.go b/pkg/authorization/rbac/rbac_test.go
index 07d1a79..3e614cf 100644
--- a/pkg/authorization/rbac/rbac_test.go
+++ b/pkg/authorization/rbac/rbac_test.go
@@ -19,6 +19,7 @@
 package rbac
 
 import (
+	"context"
 	"net/http"
 	"testing"
 
@@ -1515,7 +1516,7 @@ func TestAuthorizer_Authorize(t *testing.T) {
 				return
 			}
 
-			got, got1, err := authz.Authorize(getAuthorizerAttributes(tt.args.user, info))
+			got, got1, err := authz.Authorize(context.TODO(), getAuthorizerAttributes(tt.args.user, info))
 			if (err != nil) != tt.wantErr {
 				t.Errorf("Authorize() error = %v, wantErr %v", err, tt.wantErr)
 				return