Add authorization control for patching workspacetemplates (#5191)

* update patch workspacetemplate for supporting patch with JsonPatchType and change the authorization processing

Signed-off-by: Wenhao Zhou <wenhaozhou@yunify.com>

* make goimports

* Fix: Of the type is not string will lead to panic

Signed-off-by: Wenhao Zhou <wenhaozhou@yunify.com>

* Add jsonpatchutil for handling json patch data

Signed-off-by: Wenhao Zhou <wenhaozhou@yunify.com>

* Updated patch workspacetemplate to to make the code run more efficiently

* fix: multiple clusterrolebindings cannot autorizate

* Correct wrong spelling

Signed-off-by: Wenhao Zhou <wenhaozhou@yunify.com>
This commit is contained in:
Wenhao Zhou
2022-09-08 10:23:41 +08:00
committed by GitHub
parent 3b5fae0013
commit 95576bb827
4 changed files with 214 additions and 12 deletions

View File

@@ -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) {

View File

@@ -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 {

View File

@@ -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()
}

View File

@@ -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
}