diff --git a/pkg/kapis/tenant/v1alpha2/handler.go b/pkg/kapis/tenant/v1alpha2/handler.go index f59c1004a..ac5ce47c5 100644 --- a/pkg/kapis/tenant/v1alpha2/handler.go +++ b/pkg/kapis/tenant/v1alpha2/handler.go @@ -520,33 +520,44 @@ func (h *tenantHandler) PatchNamespace(request *restful.Request, response *restf response.WriteEntity(patched) } -func (h *tenantHandler) PatchWorkspaceTemplate(request *restful.Request, response *restful.Response) { - workspaceName := request.PathParameter("workspace") +func (h *tenantHandler) PatchWorkspaceTemplate(req *restful.Request, resp *restful.Response) { + workspaceName := req.PathParameter("workspace") var data json.RawMessage - err := request.ReadEntity(&data) + err := req.ReadEntity(&data) if err != nil { klog.Error(err) - api.HandleBadRequest(response, request, err) + api.HandleBadRequest(resp, req, err) return } - patched, err := h.tenant.PatchWorkspaceTemplate(workspaceName, data) + requestUser, ok := request.UserFrom(req.Request.Context()) + if !ok { + err := fmt.Errorf("cannot obtain user info") + klog.Errorln(err) + api.HandleForbidden(resp, req, err) + } + + patched, err := h.tenant.PatchWorkspaceTemplate(requestUser, workspaceName, data) if err != nil { klog.Error(err) if errors.IsNotFound(err) { - api.HandleNotFound(response, request, err) + api.HandleNotFound(resp, req, err) return } if errors.IsBadRequest(err) { - api.HandleBadRequest(response, request, err) + api.HandleBadRequest(resp, req, err) return } - api.HandleInternalError(response, request, err) + if errors.IsNotFound(err) { + api.HandleForbidden(resp, req, err) + return + } + api.HandleInternalError(resp, req, err) return } - response.WriteEntity(patched) + resp.WriteEntity(patched) } func (h *tenantHandler) ListClusters(r *restful.Request, response *restful.Response) { diff --git a/pkg/models/tenant/tenant.go b/pkg/models/tenant/tenant.go index 7d264e368..87b249a5d 100644 --- a/pkg/models/tenant/tenant.go +++ b/pkg/models/tenant/tenant.go @@ -25,6 +25,7 @@ import ( "time" corev1 "k8s.io/api/core/v1" + rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/errors" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/labels" @@ -69,6 +70,8 @@ import ( loggingclient "kubesphere.io/kubesphere/pkg/simple/client/logging" meteringclient "kubesphere.io/kubesphere/pkg/simple/client/metering" monitoringclient "kubesphere.io/kubesphere/pkg/simple/client/monitoring" + "kubesphere.io/kubesphere/pkg/utils/clusterclient" + jsonpatchutil "kubesphere.io/kubesphere/pkg/utils/josnpatchutil" "kubesphere.io/kubesphere/pkg/utils/stringutils" ) @@ -81,7 +84,7 @@ type Interface interface { CreateWorkspaceTemplate(workspace *tenantv1alpha2.WorkspaceTemplate) (*tenantv1alpha2.WorkspaceTemplate, error) DeleteWorkspaceTemplate(workspace string, opts metav1.DeleteOptions) error UpdateWorkspaceTemplate(workspace *tenantv1alpha2.WorkspaceTemplate) (*tenantv1alpha2.WorkspaceTemplate, error) - PatchWorkspaceTemplate(workspace string, data json.RawMessage) (*tenantv1alpha2.WorkspaceTemplate, error) + PatchWorkspaceTemplate(user user.Info, workspace string, data json.RawMessage) (*tenantv1alpha2.WorkspaceTemplate, error) DescribeWorkspaceTemplate(workspace string) (*tenantv1alpha2.WorkspaceTemplate, error) ListNamespaces(user user.Info, workspace string, query *query.Query) (*api.ListResult, error) ListDevOpsProjects(user user.Info, workspace string, query *query.Query) (*api.ListResult, error) @@ -117,6 +120,7 @@ type tenantOperator struct { auditing auditing.Interface mo monitoring.MonitoringOperator opRelease openpitrix.ReleaseInterface + clusterClient clusterclient.ClusterClients } func New(informers informers.InformerFactory, k8sclient kubernetes.Interface, ksclient kubesphere.Interface, evtsClient eventsclient.Client, loggingClient loggingclient.Client, auditingclient auditingclient.Client, am am.AccessManagementInterface, im im.IdentityManagementInterface, authorizer authorizer.Authorizer, monitoringclient monitoringclient.Interface, resourceGetter *resourcev1alpha3.ResourceGetter, opClient openpitrix.Interface) Interface { @@ -132,6 +136,7 @@ func New(informers informers.InformerFactory, k8sclient kubernetes.Interface, ks auditing: auditing.NewEventsOperator(auditingclient), mo: monitoring.NewMonitoringOperator(monitoringclient, nil, k8sclient, informers, resourceGetter, nil), opRelease: opClient, + clusterClient: clusterclient.NewClusterClient(informers.KubeSphereSharedInformerFactory().Cluster().V1alpha1().Clusters()), } } @@ -470,8 +475,116 @@ func (t *tenantOperator) PatchNamespace(workspace string, namespace *corev1.Name return t.k8sclient.CoreV1().Namespaces().Patch(context.Background(), namespace.Name, types.MergePatchType, data, metav1.PatchOptions{}) } -func (t *tenantOperator) PatchWorkspaceTemplate(workspace string, data json.RawMessage) (*tenantv1alpha2.WorkspaceTemplate, error) { - return t.ksclient.TenantV1alpha2().WorkspaceTemplates().Patch(context.Background(), workspace, types.MergePatchType, data, metav1.PatchOptions{}) +func (t *tenantOperator) PatchWorkspaceTemplate(user user.Info, workspace string, data json.RawMessage) (*tenantv1alpha2.WorkspaceTemplate, error) { + var manageWorkspaceTemplateRequest bool + clusterNames := sets.NewString() + + patchs, err := jsonpatchutil.Parse(data) + if err != nil { + klog.Error(err) + return nil, err + } + + if len(patchs) > 0 { + for _, patch := range patchs { + path, err := patch.Path() + if err != nil { + klog.Error(err) + return nil, err + } + + // If the request path is cluster, just collecting cluster name to set and continue to check cluster permission later. + // Or indicate that want to manage the workspace templates, so check if user has the permission to manage workspace templates. + if strings.HasPrefix(path, "/spec/placement/clusters/") { + if patch.Kind() != "add" && patch.Kind() != "remove" { + err := errors.NewBadRequest("not support operation type") + klog.Error(err) + return nil, err + } + clusterValue := make(map[string]string) + err := jsonpatchutil.GetValue(patch, &clusterValue) + if err != nil { + klog.Error(err) + return nil, err + } + if cName := clusterValue["name"]; cName != "" { + clusterNames.Insert(cName) + } + } else { + manageWorkspaceTemplateRequest = true + } + } + } + + if manageWorkspaceTemplateRequest { + deleteWST := authorizer.AttributesRecord{ + User: user, + Verb: authorizer.VerbDelete, + APIGroup: tenantv1alpha2.SchemeGroupVersion.Group, + APIVersion: tenantv1alpha2.SchemeGroupVersion.Version, + Resource: tenantv1alpha2.ResourcePluralWorkspaceTemplate, + ResourceRequest: true, + ResourceScope: request.GlobalScope, + } + authorize, reason, err := t.authorizer.Authorize(deleteWST) + if err != nil { + klog.Error(err) + return nil, err + } + if authorize != authorizer.DecisionAllow { + err := errors.NewForbidden(tenantv1alpha2.Resource(tenantv1alpha2.ResourcePluralWorkspaceTemplate), workspace, fmt.Errorf(reason)) + klog.Error(err) + return nil, err + } + } + // Checking whether the user can manage the cluster requires authentication from two aspects. + // First check whether the user has relevant global permissions, + // and then check whether the user has relevant cluster permissions in the target cluster + if clusterNames.Len() > 0 { + for _, clusterName := range clusterNames.List() { + deleteCluster := authorizer.AttributesRecord{ + User: user, + Verb: authorizer.VerbDelete, + APIGroup: clusterv1alpha1.SchemeGroupVersion.Version, + APIVersion: clusterv1alpha1.SchemeGroupVersion.Version, + Resource: clusterv1alpha1.ResourcesPluralCluster, + Cluster: clusterName, + ResourceRequest: true, + ResourceScope: request.GlobalScope, + } + authorize, reason, err := t.authorizer.Authorize(deleteCluster) + if err != nil { + klog.Error(err) + return nil, err + } + + if authorize == authorizer.DecisionAllow { + continue + } + + list, err := t.getClusterRoleBindingsByUser(clusterName, user.GetName()) + if err != nil { + klog.Error(err) + return nil, err + } + + allowed := false + for _, clusterRolebinding := range list.Items { + if clusterRolebinding.RoleRef.Name == iamv1alpha2.ClusterAdmin { + allowed = true + break + } + } + + if !allowed { + err = errors.NewForbidden(clusterv1alpha1.Resource(clusterv1alpha1.ResourcesPluralCluster), clusterName, fmt.Errorf(reason)) + klog.Error(err) + return nil, err + } + } + } + + return t.ksclient.TenantV1alpha2().WorkspaceTemplates().Patch(context.Background(), workspace, types.JSONPatchType, data, metav1.PatchOptions{}) } func (t *tenantOperator) CreateWorkspaceTemplate(workspace *tenantv1alpha2.WorkspaceTemplate) (*tenantv1alpha2.WorkspaceTemplate, error) { @@ -1081,6 +1194,16 @@ func (t *tenantOperator) MeteringHierarchy(user user.Info, queryParam *meteringv return resourceStats, nil } +func (t *tenantOperator) getClusterRoleBindingsByUser(clusterName, user string) (*rbacv1.ClusterRoleBindingList, error) { + kubernetesClientSet, err := t.clusterClient.GetKubernetesClientSet(clusterName) + if err != nil { + return nil, err + } + return kubernetesClientSet.RbacV1().ClusterRoleBindings(). + List(context.Background(), + metav1.ListOptions{LabelSelector: labels.FormatLabels(map[string]string{"iam.kubesphere.io/user-ref": user})}) +} + func contains(objects []runtime.Object, object runtime.Object) bool { for _, item := range objects { if item == object { diff --git a/pkg/utils/clusterclient/clusterclient.go b/pkg/utils/clusterclient/clusterclient.go index fdf7c874c..f4dd07216 100644 --- a/pkg/utils/clusterclient/clusterclient.go +++ b/pkg/utils/clusterclient/clusterclient.go @@ -23,6 +23,7 @@ import ( "sync" corev1 "k8s.io/api/core/v1" + "k8s.io/client-go/kubernetes" "k8s.io/client-go/rest" "k8s.io/client-go/tools/cache" "k8s.io/client-go/tools/clientcmd" @@ -30,6 +31,7 @@ import ( clusterv1alpha1 "kubesphere.io/api/cluster/v1alpha1" + kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned" clusterinformer "kubesphere.io/kubesphere/pkg/client/informers/externalversions/cluster/v1alpha1" clusterlister "kubesphere.io/kubesphere/pkg/client/listers/cluster/v1alpha1" ) @@ -54,6 +56,8 @@ type ClusterClients interface { GetClusterKubeconfig(string) (string, error) Get(string) (*clusterv1alpha1.Cluster, error) GetInnerCluster(string) *innerCluster + GetKubernetesClientSet(string) (*kubernetes.Clientset, error) + GetKubeSphereClientSet(string) (*kubesphere.Clientset, error) } func NewClusterClient(clusterInformer clusterinformer.ClusterInformer) ClusterClients { @@ -182,3 +186,45 @@ func (c *clusterClients) IsHostCluster(cluster *clusterv1alpha1.Cluster) bool { } return false } + +func (c *clusterClients) GetKubeSphereClientSet(name string) (*kubesphere.Clientset, error) { + kubeconfig, err := c.GetClusterKubeconfig(name) + if err != nil { + return nil, err + } + restConfig, err := newRestConfigFromString(kubeconfig) + if err != nil { + return nil, err + } + clientSet, err := kubesphere.NewForConfig(restConfig) + if err != nil { + return nil, err + } + + return clientSet, nil +} + +func (c *clusterClients) GetKubernetesClientSet(name string) (*kubernetes.Clientset, error) { + kubeconfig, err := c.GetClusterKubeconfig(name) + if err != nil { + return nil, err + } + restConfig, err := newRestConfigFromString(kubeconfig) + if err != nil { + return nil, err + } + clientSet, err := kubernetes.NewForConfig(restConfig) + if err != nil { + return nil, err + } + + return clientSet, nil +} + +func newRestConfigFromString(kubeconfig string) (*rest.Config, error) { + bytes, err := clientcmd.NewClientConfigFromBytes([]byte(kubeconfig)) + if err != nil { + return nil, err + } + return bytes.ClientConfig() +} diff --git a/pkg/utils/josnpatchutil/jsonpatchutil.go b/pkg/utils/josnpatchutil/jsonpatchutil.go new file mode 100644 index 000000000..0f79cba14 --- /dev/null +++ b/pkg/utils/josnpatchutil/jsonpatchutil.go @@ -0,0 +1,22 @@ +package josnpatchutil + +import ( + jsonpatch "github.com/evanphx/json-patch" + "github.com/mitchellh/mapstructure" +) + +func Parse(raw []byte) (jsonpatch.Patch, error) { + return jsonpatch.DecodePatch(raw) +} + +func GetValue(patch jsonpatch.Operation, value interface{}) error { + valueInterface, err := patch.ValueInterface() + if err != nil { + return err + } + + if err := mapstructure.Decode(valueInterface, value); err != nil { + return err + } + return nil +}