diff --git a/pkg/apis/iam/v1alpha2/groupbinding_types.go b/pkg/apis/iam/v1alpha2/groupbinding_types.go index 7e120cd88..3ad07762c 100644 --- a/pkg/apis/iam/v1alpha2/groupbinding_types.go +++ b/pkg/apis/iam/v1alpha2/groupbinding_types.go @@ -20,6 +20,10 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" ) +const ( + ResourcePluralGroupBinding = "groupbindings" +) + // GroupRef defines the desired relation of GroupBinding type GroupRef struct { APIGroup string `json:"apiGroup,omitempty"` diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 141482110..f610f7d65 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -20,6 +20,10 @@ import ( "bytes" "context" "fmt" + "net/http" + rt "runtime" + "time" + "github.com/emicklei/go-restful" "k8s.io/apimachinery/pkg/runtime/schema" urlruntime "k8s.io/apimachinery/pkg/util/runtime" @@ -65,6 +69,7 @@ import ( terminalv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/terminal/v1alpha2" "kubesphere.io/kubesphere/pkg/kapis/version" "kubesphere.io/kubesphere/pkg/models/iam/am" + "kubesphere.io/kubesphere/pkg/models/iam/group" "kubesphere.io/kubesphere/pkg/models/iam/im" "kubesphere.io/kubesphere/pkg/simple/client/auditing" "kubesphere.io/kubesphere/pkg/simple/client/cache" @@ -77,9 +82,6 @@ import ( "kubesphere.io/kubesphere/pkg/simple/client/s3" "kubesphere.io/kubesphere/pkg/simple/client/sonarqube" utilnet "kubesphere.io/kubesphere/pkg/utils/net" - "net/http" - rt "runtime" - "time" ) const ( @@ -183,6 +185,7 @@ func (s *APIServer) installKubeSphereAPIs() { imOperator := im.NewOperator(s.KubernetesClient.KubeSphere(), s.InformerFactory, s.Config.AuthenticationOptions) urlruntime.Must(iamapi.AddToContainer(s.container, imOperator, am.NewOperator(s.InformerFactory, s.KubernetesClient.KubeSphere(), s.KubernetesClient.Kubernetes()), + group.New(s.InformerFactory, s.KubernetesClient.KubeSphere(), s.KubernetesClient.Kubernetes()), s.Config.AuthenticationOptions)) urlruntime.Must(oauth.AddToContainer(s.container, imOperator, @@ -375,6 +378,8 @@ func (s *APIServer) waitForResourceSync(stopCh <-chan struct{}) error { {Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "users"}, {Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "globalroles"}, {Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "globalrolebindings"}, + {Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "groups"}, + {Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "groupbindings"}, {Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "workspaceroles"}, {Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "workspacerolebindings"}, {Group: "iam.kubesphere.io", Version: "v1alpha2", Resource: "loginrecords"}, diff --git a/pkg/kapis/iam/v1alpha2/handler.go b/pkg/kapis/iam/v1alpha2/handler.go index 8f18a69ba..4e6ba7d48 100644 --- a/pkg/kapis/iam/v1alpha2/handler.go +++ b/pkg/kapis/iam/v1alpha2/handler.go @@ -18,6 +18,8 @@ package v1alpha2 import ( "fmt" + "strings" + "github.com/emicklei/go-restful" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/errors" @@ -32,21 +34,23 @@ import ( "kubesphere.io/kubesphere/pkg/apiserver/query" apirequest "kubesphere.io/kubesphere/pkg/apiserver/request" "kubesphere.io/kubesphere/pkg/models/iam/am" + "kubesphere.io/kubesphere/pkg/models/iam/group" "kubesphere.io/kubesphere/pkg/models/iam/im" servererr "kubesphere.io/kubesphere/pkg/server/errors" - "strings" ) type iamHandler struct { am am.AccessManagementInterface im im.IdentityManagementInterface + group group.GroupOperator authorizer authorizer.Authorizer } -func newIAMHandler(im im.IdentityManagementInterface, am am.AccessManagementInterface, options *authoptions.AuthenticationOptions) *iamHandler { +func newIAMHandler(im im.IdentityManagementInterface, am am.AccessManagementInterface, group group.GroupOperator, options *authoptions.AuthenticationOptions) *iamHandler { return &iamHandler{ am: am, im: im, + group: group, authorizer: authorizerfactory.NewRBACAuthorizer(am), } } @@ -56,6 +60,11 @@ type Member struct { RoleRef string `json:"roleRef"` } +type GroupMember struct { + UserName string `json:"userName"` + GroupName string `json:"groupName"` +} + func (h *iamHandler) DescribeUser(request *restful.Request, response *restful.Response) { username := request.PathParameter("user") @@ -1274,3 +1283,223 @@ func handleError(request *restful.Request, response *restful.Response, err error api.HandleInternalError(response, request, err) } } + +func (h *iamHandler) ListWorkspaceGroups(request *restful.Request, response *restful.Response) { + workspaceName := request.PathParameter("workspace") + queryParam := query.ParseQueryParameter(request) + result, err := h.group.ListGroups(workspaceName, queryParam) + + if err != nil { + klog.Error(err) + if errors.IsNotFound(err) { + api.HandleNotFound(response, request, err) + return + } + api.HandleInternalError(response, request, err) + return + } + + response.WriteEntity(result) +} + +func (h *iamHandler) CreateGroup(request *restful.Request, response *restful.Response) { + workspace := request.PathParameter("workspace") + var group iamv1alpha2.Group + + err := request.ReadEntity(&group) + + if err != nil { + klog.Error(err) + api.HandleBadRequest(response, request, err) + return + } + + created, err := h.group.CreateGroup(workspace, &group) + + if err != nil { + klog.Error(err) + if errors.IsNotFound(err) { + api.HandleNotFound(response, request, err) + return + } + api.HandleBadRequest(response, request, err) + return + } + + response.WriteEntity(created) +} + +func (h *iamHandler) DescribeGroup(request *restful.Request, response *restful.Response) { + workspaceName := request.PathParameter("workspace") + groupName := request.PathParameter("group") + ns, err := h.group.DescribeGroup(workspaceName, groupName) + + if err != nil { + if errors.IsNotFound(err) { + api.HandleNotFound(response, request, err) + return + } + api.HandleInternalError(response, request, err) + return + } + + response.WriteEntity(ns) +} + +func (h *iamHandler) DeleteGroup(request *restful.Request, response *restful.Response) { + workspaceName := request.PathParameter("workspace") + groupName := request.PathParameter("group") + + err := h.group.DeleteGroup(workspaceName, groupName) + + if err != nil { + if errors.IsNotFound(err) { + api.HandleNotFound(response, request, err) + return + } + api.HandleInternalError(response, request, err) + return + } + + response.WriteEntity(servererr.None) +} + +func (h *iamHandler) UpdateGroup(request *restful.Request, response *restful.Response) { + workspaceName := request.PathParameter("workspace") + groupName := request.PathParameter("group") + + var group iamv1alpha2.Group + err := request.ReadEntity(&group) + if err != nil { + klog.Error(err) + api.HandleBadRequest(response, request, err) + return + } + + if groupName != group.Name { + err := fmt.Errorf("the name of the object (%s) does not match the name on the URL (%s)", group.Name, groupName) + klog.Errorf("%+v", err) + api.HandleBadRequest(response, request, err) + return + } + + updated, err := h.group.UpdateGroup(workspaceName, &group) + + if err != nil { + klog.Error(err) + if errors.IsNotFound(err) { + api.HandleNotFound(response, request, err) + return + } + if errors.IsBadRequest(err) { + api.HandleBadRequest(response, request, err) + return + } + api.HandleInternalError(response, request, err) + return + } + + response.WriteEntity(updated) +} + +func (h *iamHandler) PatchGroup(request *restful.Request, response *restful.Response) { + workspaceName := request.PathParameter("workspace") + groupName := request.PathParameter("group") + + var group iamv1alpha2.Group + err := request.ReadEntity(&group) + if err != nil { + klog.Error(err) + api.HandleBadRequest(response, request, err) + return + } + + group.Name = groupName + + patched, err := h.group.PatchGroup(workspaceName, &group) + + if err != nil { + klog.Error(err) + if errors.IsNotFound(err) { + api.HandleNotFound(response, request, err) + return + } + if errors.IsBadRequest(err) { + api.HandleBadRequest(response, request, err) + return + } + api.HandleInternalError(response, request, err) + return + } + + response.WriteEntity(patched) +} + +func (h *iamHandler) ListGroupBindings(request *restful.Request, response *restful.Response) { + workspaceName := request.PathParameter("workspace") + groupName := request.PathParameter("group") + queryParam := query.ParseQueryParameter(request) + result, err := h.group.ListGroupBindings(workspaceName, groupName, queryParam) + + if err != nil { + klog.Error(err) + if errors.IsNotFound(err) { + api.HandleNotFound(response, request, err) + return + } + api.HandleInternalError(response, request, err) + return + } + + response.WriteEntity(result) +} + +func (h *iamHandler) ListGroupsRoleBinding(request *restful.Request, response *restful.Response) { + //todo +} + +func (h *iamHandler) ListGroupsWorkspaceRoleBinding(request *restful.Request, response *restful.Response) { + //todo +} + +func (h *iamHandler) CreateGroupBinding(request *restful.Request, response *restful.Response) { + + workspace := request.PathParameter("workspace") + + var members []GroupMember + err := request.ReadEntity(&members) + if err != nil { + klog.Error(err) + api.HandleBadRequest(response, request, err) + return + } + + for _, item := range members { + err := h.group.CreateGroupBinding(workspace, item.GroupName, item.UserName) + if err != nil { + klog.Error(err) + handleError(request, response, err) + return + } + } + + response.WriteEntity(members) +} + +func (h *iamHandler) DeleteGroupBinding(request *restful.Request, response *restful.Response) { + workspaceName := request.PathParameter("workspace") + name := request.PathParameter("groupbinding") + + err := h.group.DeleteGroupBinding(workspaceName, name) + + if err != nil { + if errors.IsNotFound(err) { + api.HandleNotFound(response, request, err) + return + } + api.HandleInternalError(response, request, err) + return + } + + response.WriteEntity(servererr.None) +} diff --git a/pkg/kapis/iam/v1alpha2/register.go b/pkg/kapis/iam/v1alpha2/register.go index 06a50b08f..3f942e958 100644 --- a/pkg/kapis/iam/v1alpha2/register.go +++ b/pkg/kapis/iam/v1alpha2/register.go @@ -17,8 +17,10 @@ limitations under the License. package v1alpha2 import ( + "net/http" + "github.com/emicklei/go-restful" - "github.com/emicklei/go-restful-openapi" + restfulspec "github.com/emicklei/go-restful-openapi" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/runtime/schema" "kubesphere.io/kubesphere/pkg/api" @@ -28,9 +30,9 @@ import ( "kubesphere.io/kubesphere/pkg/apiserver/runtime" "kubesphere.io/kubesphere/pkg/constants" "kubesphere.io/kubesphere/pkg/models/iam/am" + "kubesphere.io/kubesphere/pkg/models/iam/group" "kubesphere.io/kubesphere/pkg/models/iam/im" "kubesphere.io/kubesphere/pkg/server/errors" - "net/http" ) const ( @@ -39,9 +41,9 @@ const ( var GroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha2"} -func AddToContainer(container *restful.Container, im im.IdentityManagementInterface, am am.AccessManagementInterface, options *authoptions.AuthenticationOptions) error { +func AddToContainer(container *restful.Container, im im.IdentityManagementInterface, am am.AccessManagementInterface, group group.GroupOperator, options *authoptions.AuthenticationOptions) error { ws := runtime.NewWebService(GroupVersion) - handler := newIAMHandler(im, am, options) + handler := newIAMHandler(im, am, group, options) // users ws.Route(ws.POST("/users"). @@ -476,6 +478,86 @@ func AddToContainer(container *restful.Container, im im.IdentityManagementInterf Returns(http.StatusOK, api.StatusOK, api.ListResult{Items: []interface{}{rbacv1.Role{}}}). Metadata(restfulspec.KeyOpenAPITags, []string{constants.DevOpsProjectRoleTag})) + ws.Route(ws.GET("/workspaces/{workspace}/groups"). + To(handler.ListWorkspaceGroups). + Param(ws.PathParameter("workspace", "workspace name")). + Returns(http.StatusOK, api.StatusOK, api.ListResult{}). + Doc("List groups of the specified workspace."). + Metadata(restfulspec.KeyOpenAPITags, []string{constants.GroupTag})) + + ws.Route(ws.GET("/workspaces/{workspace}/groups/{group}"). + To(handler.DescribeGroup). + Param(ws.PathParameter("workspace", "workspace name")). + Param(ws.PathParameter("group", "group name")). + Doc("Retrieve group details."). + Returns(http.StatusOK, api.StatusOK, iamv1alpha2.Group{}). + Metadata(restfulspec.KeyOpenAPITags, []string{constants.GroupTag})) + + ws.Route(ws.DELETE("/workspaces/{workspace}/groups/{group}"). + To(handler.DeleteGroup). + Param(ws.PathParameter("workspace", "workspace name")). + Param(ws.PathParameter("group", "group name")). + Doc("Delete group."). + Returns(http.StatusOK, api.StatusOK, errors.None). + Metadata(restfulspec.KeyOpenAPITags, []string{constants.GroupTag})) + + ws.Route(ws.POST("/workspaces/{workspace}/groups"). + To(handler.CreateGroup). + Param(ws.PathParameter("workspace", "workspace name")). + Doc("Create Group"). + Reads(iamv1alpha2.Group{}). + Returns(http.StatusOK, api.StatusOK, iamv1alpha2.Group{}). + Metadata(restfulspec.KeyOpenAPITags, []string{constants.GroupTag})) + + ws.Route(ws.PUT("/workspaces/{workspace}/groups/{group}/"). + To(handler.UpdateGroup). + Param(ws.PathParameter("workspace", "workspace name")). + Param(ws.PathParameter("group", "group name")). + Doc("Update Group"). + Reads(iamv1alpha2.Group{}). + Returns(http.StatusOK, api.StatusOK, iamv1alpha2.Group{}). + Metadata(restfulspec.KeyOpenAPITags, []string{constants.GroupTag})) + + ws.Route(ws.GET("/workspaces/{workspace}/groups/{group}/groupbindings"). + To(handler.ListGroupBindings). + Param(ws.PathParameter("workspace", "workspace name")). + Param(ws.PathParameter("group", "group name")). + Doc("Retrieve group's members in the workspace."). + Returns(http.StatusOK, api.StatusOK, api.ListResult{}). + Metadata(restfulspec.KeyOpenAPITags, []string{constants.GroupTag})) + + ws.Route(ws.GET("/workspaces/{workspace}/groups/{group}/rolebindings"). + To(handler.ListGroupsRoleBinding). + Param(ws.PathParameter("workspace", "workspace name")). + Param(ws.PathParameter("group", "group name")). + Doc("Retrieve group's rolebindings of all projects in the workspace."). + Returns(http.StatusOK, api.StatusOK, api.ListResult{}). + Metadata(restfulspec.KeyOpenAPITags, []string{constants.GroupTag})) + + ws.Route(ws.GET("/workspaces/{workspace}/groups/{group}/workspacerolebinding"). + To(handler.ListGroupsWorkspaceRoleBinding). + Param(ws.PathParameter("workspace", "workspace name")). + Param(ws.PathParameter("group", "group name")). + Doc("Retrieve group's workspacerolebindings of the workspace."). + Returns(http.StatusOK, api.StatusOK, api.ListResult{}). + Metadata(restfulspec.KeyOpenAPITags, []string{constants.GroupTag})) + + ws.Route(ws.DELETE("/workspaces/{workspace}/groupbindings/{groupbinding}"). + To(handler.DeleteGroupBinding). + Param(ws.PathParameter("workspace", "workspace name")). + Param(ws.PathParameter("groupbinding", "groupbinding name")). + Doc("Delete GroupBinding to remove user from the group."). + Returns(http.StatusOK, api.StatusOK, errors.None). + Metadata(restfulspec.KeyOpenAPITags, []string{constants.GroupTag})) + + ws.Route(ws.POST("/workspaces/{workspace}/groupbindings"). + To(handler.CreateGroupBinding). + Param(ws.PathParameter("workspace", "workspace name")). + Doc("Create GroupBinding to add a user to the group"). + Reads([]GroupMember{}). + Returns(http.StatusOK, api.StatusOK, iamv1alpha2.GroupBinding{}). + Metadata(restfulspec.KeyOpenAPITags, []string{constants.GroupTag})) + container.Add(ws) return nil } diff --git a/pkg/models/iam/group/group.go b/pkg/models/iam/group/group.go new file mode 100644 index 000000000..1bdb3c159 --- /dev/null +++ b/pkg/models/iam/group/group.go @@ -0,0 +1,196 @@ +/* +Copyright 2020 KubeSphere 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 group + +import ( + "encoding/json" + "fmt" + + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/types" + "k8s.io/client-go/kubernetes" + "k8s.io/klog" + "kubesphere.io/kubesphere/pkg/api" + iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2" + tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1" + "kubesphere.io/kubesphere/pkg/apiserver/query" + kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned" + "kubesphere.io/kubesphere/pkg/informers" + resourcesv1alpha3 "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/resource" +) + +type GroupOperator interface { + ListGroups(workspace string, queryParam *query.Query) (*api.ListResult, error) + CreateGroup(workspace string, namespace *iamv1alpha2.Group) (*iamv1alpha2.Group, error) + DescribeGroup(workspace, group string) (*iamv1alpha2.Group, error) + DeleteGroup(workspace, group string) error + UpdateGroup(workspace string, group *iamv1alpha2.Group) (*iamv1alpha2.Group, error) + PatchGroup(workspace string, group *iamv1alpha2.Group) (*iamv1alpha2.Group, error) + DeleteGroupBinding(workspace, name string) error + CreateGroupBinding(workspace, groupName, userName string) error + ListGroupBindings(workspace, group string, queryParam *query.Query) (*api.ListResult, error) +} + +type groupOperator struct { + k8sclient kubernetes.Interface + ksclient kubesphere.Interface + resourceGetter *resourcesv1alpha3.ResourceGetter +} + +func New(informers informers.InformerFactory, ksclient kubesphere.Interface, k8sclient kubernetes.Interface) GroupOperator { + return &groupOperator{ + resourceGetter: resourcesv1alpha3.NewResourceGetter(informers), + k8sclient: k8sclient, + ksclient: ksclient, + } +} + +func (t *groupOperator) ListGroups(workspace string, queryParam *query.Query) (*api.ListResult, error) { + + if workspace != "" { + // filter by workspace + queryParam.Filters[query.FieldLabel] = query.Value(fmt.Sprintf("%s=%s", tenantv1alpha1.WorkspaceLabel, workspace)) + } + + result, err := t.resourceGetter.List("groups", "", queryParam) + if err != nil { + klog.Error(err) + return nil, err + } + return result, nil +} + +// CreateGroup adds a workspace label to group which indicates group is under the workspace +func (t *groupOperator) CreateGroup(workspace string, namespace *iamv1alpha2.Group) (*iamv1alpha2.Group, error) { + return t.ksclient.IamV1alpha2().Groups().Create(labelGroupWithWorkspaceName(namespace, workspace)) +} + +func (t *groupOperator) DescribeGroup(workspace, group string) (*iamv1alpha2.Group, error) { + obj, err := t.resourceGetter.Get("groups", "", group) + if err != nil { + return nil, err + } + ns := obj.(*iamv1alpha2.Group) + if ns.Labels[tenantv1alpha1.WorkspaceLabel] != workspace { + err := errors.NewNotFound(corev1.Resource("group"), group) + klog.Error(err) + return nil, err + } + return ns, nil +} + +func (t *groupOperator) DeleteGroup(workspace, group string) error { + _, err := t.DescribeGroup(workspace, group) + if err != nil { + return err + } + return t.ksclient.IamV1alpha2().Groups().Delete(group, metav1.NewDeleteOptions(0)) +} + +func (t *groupOperator) UpdateGroup(workspace string, group *iamv1alpha2.Group) (*iamv1alpha2.Group, error) { + _, err := t.DescribeGroup(workspace, group.Name) + if err != nil { + return nil, err + } + group = labelGroupWithWorkspaceName(group, workspace) + return t.ksclient.IamV1alpha2().Groups().Update(group) +} + +func (t *groupOperator) PatchGroup(workspace string, group *iamv1alpha2.Group) (*iamv1alpha2.Group, error) { + _, err := t.DescribeGroup(workspace, group.Name) + if err != nil { + return nil, err + } + if group.Labels != nil { + group.Labels[tenantv1alpha1.WorkspaceLabel] = workspace + } + data, err := json.Marshal(group) + if err != nil { + return nil, err + } + return t.ksclient.IamV1alpha2().Groups().Patch(group.Name, types.MergePatchType, data) +} + +func (t *groupOperator) DeleteGroupBinding(workspace, name string) error { + obj, err := t.resourceGetter.Get("groupbindings", "", name) + if err != nil { + return err + } + ns := obj.(*iamv1alpha2.GroupBinding) + if ns.Labels[tenantv1alpha1.WorkspaceLabel] != workspace { + err := errors.NewNotFound(corev1.Resource("groupbinding"), name) + klog.Error(err) + return err + } + + return t.ksclient.IamV1alpha2().GroupBindings().Delete(name, metav1.NewDeleteOptions(0)) +} + +func (t *groupOperator) CreateGroupBinding(workspace, groupName, userName string) error { + + groupBinding := iamv1alpha2.GroupBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: fmt.Sprintf("%s-%s", groupName, userName), + Labels: map[string]string{ + iamv1alpha2.UserReferenceLabel: userName, + iamv1alpha2.GroupReferenceLabel: groupName, + tenantv1alpha1.WorkspaceLabel: workspace, + }, + }, + Users: []string{userName}, + GroupRef: iamv1alpha2.GroupRef{ + APIGroup: iamv1alpha2.SchemeGroupVersion.Group, + Kind: iamv1alpha2.ResourcePluralGroup, + Name: groupName, + }, + } + + if _, err := t.ksclient.IamV1alpha2().GroupBindings().Create(&groupBinding); err != nil { + return err + } + + return nil +} + +func (t *groupOperator) ListGroupBindings(workspace, group string, queryParam *query.Query) (*api.ListResult, error) { + + if group != "" && workspace != "" { + // filter by group + queryParam.Filters[query.FieldLabel] = query.Value(fmt.Sprintf("%s=%s", iamv1alpha2.GroupReferenceLabel, group)) + } + + result, err := t.resourceGetter.List("groupbindings", "", queryParam) + if err != nil { + klog.Error(err) + return nil, err + } + return result, nil +} + +// labelGroupWithWorkspaceName adds a kubesphere.io/workspace=[workspaceName] label to namespace which +// indicates namespace is under the workspace +func labelGroupWithWorkspaceName(namespace *iamv1alpha2.Group, workspaceName string) *iamv1alpha2.Group { + if namespace.Labels == nil { + namespace.Labels = make(map[string]string, 0) + } + + namespace.Labels[tenantv1alpha1.WorkspaceLabel] = workspaceName // label namespace with workspace name + + return namespace +} diff --git a/pkg/models/resources/v1alpha3/group/group.go b/pkg/models/resources/v1alpha3/group/group.go new file mode 100644 index 000000000..3c49acc98 --- /dev/null +++ b/pkg/models/resources/v1alpha3/group/group.go @@ -0,0 +1,78 @@ +/* +Copyright 2020 KubeSphere 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 group + +import ( + "k8s.io/apimachinery/pkg/runtime" + "kubesphere.io/kubesphere/pkg/api" + "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2" + "kubesphere.io/kubesphere/pkg/apiserver/query" + informers "kubesphere.io/kubesphere/pkg/client/informers/externalversions" + "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3" +) + +type groupGetter struct { + sharedInformers informers.SharedInformerFactory +} + +func New(sharedInformers informers.SharedInformerFactory) v1alpha3.Interface { + return &groupGetter{sharedInformers: sharedInformers} +} + +func (d *groupGetter) Get(_, name string) (runtime.Object, error) { + return d.sharedInformers.Iam().V1alpha2().Groups().Lister().Get(name) +} + +func (d *groupGetter) List(_ string, query *query.Query) (*api.ListResult, error) { + + groups, err := d.sharedInformers.Iam().V1alpha2().Groups().Lister().List(query.Selector()) + if err != nil { + return nil, err + } + + var result []runtime.Object + for _, group := range groups { + result = append(result, group) + } + + return v1alpha3.DefaultList(result, query, d.compare, d.filter), nil +} + +func (d *groupGetter) compare(left runtime.Object, right runtime.Object, field query.Field) bool { + + leftGroup, ok := left.(*v1alpha2.Group) + if !ok { + return false + } + + rightGroup, ok := right.(*v1alpha2.Group) + if !ok { + return false + } + + return v1alpha3.DefaultObjectMetaCompare(leftGroup.ObjectMeta, rightGroup.ObjectMeta, field) +} + +func (d *groupGetter) filter(object runtime.Object, filter query.Filter) bool { + group, ok := object.(*v1alpha2.Group) + + if !ok { + return false + } + + return v1alpha3.DefaultObjectMetaFilter(group.ObjectMeta, filter) +} diff --git a/pkg/models/resources/v1alpha3/group/group_test.go b/pkg/models/resources/v1alpha3/group/group_test.go new file mode 100644 index 000000000..b0f643ff8 --- /dev/null +++ b/pkg/models/resources/v1alpha3/group/group_test.go @@ -0,0 +1,111 @@ +/* +Copyright 2020 KubeSphere 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 group + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "kubesphere.io/kubesphere/pkg/api" + "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2" + "kubesphere.io/kubesphere/pkg/apiserver/query" + "kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake" + informers "kubesphere.io/kubesphere/pkg/client/informers/externalversions" + "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3" +) + +func TestListWorkspaces(t *testing.T) { + tests := []struct { + description string + namespace string + query *query.Query + expected *api.ListResult + expectedErr error + }{ + { + "test name filter", + "bar", + &query.Query{ + Pagination: &query.Pagination{ + Limit: 1, + Offset: 0, + }, + SortBy: query.FieldName, + Ascending: false, + Filters: map[query.Field]query.Value{query.FieldName: query.Value("foo2")}, + }, + &api.ListResult{ + Items: []interface{}{ + foo2, + }, + TotalItems: 1, + }, + nil, + }, + } + + getter := prepare() + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + + got, err := getter.List(test.namespace, test.query) + + if test.expectedErr != nil && err != test.expectedErr { + t.Errorf("expected error, got nothing") + } else if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(got, test.expected); diff != "" { + t.Errorf("%T differ (-got, +want): %s", test.expected, diff) + } + }) + } +} + +var ( + foo1 = &v1alpha2.Group{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo1", + }, + } + + foo2 = &v1alpha2.Group{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo2", + }, + } + bar1 = &v1alpha2.Group{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar1", + }, + } + + groups = []interface{}{foo1, foo2, bar1} +) + +func prepare() v1alpha3.Interface { + client := fake.NewSimpleClientset() + informer := informers.NewSharedInformerFactory(client, 0) + + for _, group := range groups { + informer.Iam().V1alpha2().Groups().Informer().GetIndexer().Add(group) + } + return New(informer) +} diff --git a/pkg/models/resources/v1alpha3/groupbinding/groupbinding.go b/pkg/models/resources/v1alpha3/groupbinding/groupbinding.go new file mode 100644 index 000000000..9acfbe380 --- /dev/null +++ b/pkg/models/resources/v1alpha3/groupbinding/groupbinding.go @@ -0,0 +1,78 @@ +/* +Copyright 2020 KubeSphere 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 groupbinding + +import ( + "k8s.io/apimachinery/pkg/runtime" + "kubesphere.io/kubesphere/pkg/api" + "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2" + "kubesphere.io/kubesphere/pkg/apiserver/query" + informers "kubesphere.io/kubesphere/pkg/client/informers/externalversions" + "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3" +) + +type groupBindingGetter struct { + sharedInformers informers.SharedInformerFactory +} + +func New(sharedInformers informers.SharedInformerFactory) v1alpha3.Interface { + return &groupBindingGetter{sharedInformers: sharedInformers} +} + +func (d *groupBindingGetter) Get(_, name string) (runtime.Object, error) { + return d.sharedInformers.Iam().V1alpha2().GroupBindings().Lister().Get(name) +} + +func (d *groupBindingGetter) List(_ string, query *query.Query) (*api.ListResult, error) { + + groupBindings, err := d.sharedInformers.Iam().V1alpha2().GroupBindings().Lister().List(query.Selector()) + if err != nil { + return nil, err + } + + var result []runtime.Object + for _, groupBinding := range groupBindings { + result = append(result, groupBinding) + } + + return v1alpha3.DefaultList(result, query, d.compare, d.filter), nil +} + +func (d *groupBindingGetter) compare(left runtime.Object, right runtime.Object, field query.Field) bool { + + leftGroupBinding, ok := left.(*v1alpha2.GroupBinding) + if !ok { + return false + } + + rightGroupBinding, ok := right.(*v1alpha2.GroupBinding) + if !ok { + return false + } + + return v1alpha3.DefaultObjectMetaCompare(leftGroupBinding.ObjectMeta, rightGroupBinding.ObjectMeta, field) +} + +func (d *groupBindingGetter) filter(object runtime.Object, filter query.Filter) bool { + groupbinding, ok := object.(*v1alpha2.GroupBinding) + + if !ok { + return false + } + + return v1alpha3.DefaultObjectMetaFilter(groupbinding.ObjectMeta, filter) +} diff --git a/pkg/models/resources/v1alpha3/groupbinding/groupbinding_test.go b/pkg/models/resources/v1alpha3/groupbinding/groupbinding_test.go new file mode 100644 index 000000000..8f30f7665 --- /dev/null +++ b/pkg/models/resources/v1alpha3/groupbinding/groupbinding_test.go @@ -0,0 +1,111 @@ +/* +Copyright 2020 KubeSphere 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 groupbinding + +import ( + "testing" + + "github.com/google/go-cmp/cmp" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "kubesphere.io/kubesphere/pkg/api" + "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2" + "kubesphere.io/kubesphere/pkg/apiserver/query" + "kubesphere.io/kubesphere/pkg/client/clientset/versioned/fake" + informers "kubesphere.io/kubesphere/pkg/client/informers/externalversions" + "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3" +) + +func TestListWorkspaces(t *testing.T) { + tests := []struct { + description string + namespace string + query *query.Query + expected *api.ListResult + expectedErr error + }{ + { + "test name filter", + "bar", + &query.Query{ + Pagination: &query.Pagination{ + Limit: 1, + Offset: 0, + }, + SortBy: query.FieldName, + Ascending: false, + Filters: map[query.Field]query.Value{query.FieldName: query.Value("foo2")}, + }, + &api.ListResult{ + Items: []interface{}{ + foo2, + }, + TotalItems: 1, + }, + nil, + }, + } + + getter := prepare() + + for _, test := range tests { + t.Run(test.description, func(t *testing.T) { + + got, err := getter.List(test.namespace, test.query) + + if test.expectedErr != nil && err != test.expectedErr { + t.Errorf("expected error, got nothing") + } else if err != nil { + t.Fatal(err) + } + + if diff := cmp.Diff(got, test.expected); diff != "" { + t.Errorf("%T differ (-got, +want): %s", test.expected, diff) + } + }) + } +} + +var ( + foo1 = &v1alpha2.GroupBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo1", + }, + } + + foo2 = &v1alpha2.GroupBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "foo2", + }, + } + bar1 = &v1alpha2.GroupBinding{ + ObjectMeta: metav1.ObjectMeta{ + Name: "bar1", + }, + } + + groupBindings = []interface{}{foo1, foo2, bar1} +) + +func prepare() v1alpha3.Interface { + client := fake.NewSimpleClientset() + informer := informers.NewSharedInformerFactory(client, 0) + + for _, groupBinding := range groupBindings { + informer.Iam().V1alpha2().GroupBindings().Informer().GetIndexer().Add(groupBinding) + } + return New(informer) +} diff --git a/pkg/models/resources/v1alpha3/resource/resource.go b/pkg/models/resources/v1alpha3/resource/resource.go index ab8c477a2..83c8b7d6a 100644 --- a/pkg/models/resources/v1alpha3/resource/resource.go +++ b/pkg/models/resources/v1alpha3/resource/resource.go @@ -18,6 +18,7 @@ package resource import ( "errors" + snapshotv1beta1 "github.com/kubernetes-csi/external-snapshotter/v2/pkg/apis/volumesnapshot/v1beta1" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/runtime" @@ -51,6 +52,8 @@ import ( "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/federatedstatefulset" "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/globalrole" "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/globalrolebinding" + "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/group" + "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/groupbinding" "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/ingress" "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/job" "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3/loginrecord" @@ -103,6 +106,8 @@ func NewResourceGetter(factory informers.InformerFactory) *ResourceGetter { getters[iamv1alpha2.SchemeGroupVersion.WithResource(iamv1alpha2.ResourcesPluralGlobalRoleBinding)] = globalrolebinding.New(factory.KubeSphereSharedInformerFactory()) getters[iamv1alpha2.SchemeGroupVersion.WithResource(iamv1alpha2.ResourcesPluralWorkspaceRoleBinding)] = workspacerolebinding.New(factory.KubeSphereSharedInformerFactory()) getters[iamv1alpha2.SchemeGroupVersion.WithResource(iamv1alpha2.ResourcesPluralLoginRecord)] = loginrecord.New(factory.KubeSphereSharedInformerFactory()) + getters[iamv1alpha2.SchemeGroupVersion.WithResource(iamv1alpha2.ResourcePluralGroup)] = group.New(factory.KubeSphereSharedInformerFactory()) + getters[iamv1alpha2.SchemeGroupVersion.WithResource(iamv1alpha2.ResourcePluralGroupBinding)] = groupbinding.New(factory.KubeSphereSharedInformerFactory()) getters[rbacv1.SchemeGroupVersion.WithResource(iamv1alpha2.ResourcesPluralRole)] = role.New(factory.KubernetesSharedInformerFactory()) getters[rbacv1.SchemeGroupVersion.WithResource(iamv1alpha2.ResourcesPluralClusterRole)] = clusterrole.New(factory.KubernetesSharedInformerFactory()) getters[rbacv1.SchemeGroupVersion.WithResource(iamv1alpha2.ResourcesPluralRoleBinding)] = rolebinding.New(factory.KubernetesSharedInformerFactory()) diff --git a/tools/cmd/doc-gen/main.go b/tools/cmd/doc-gen/main.go index 01f595c8a..2b9eca577 100644 --- a/tools/cmd/doc-gen/main.go +++ b/tools/cmd/doc-gen/main.go @@ -21,14 +21,16 @@ import ( "encoding/json" "flag" "fmt" + "io/ioutil" + "log" + "github.com/emicklei/go-restful" - "github.com/emicklei/go-restful-openapi" + restfulspec "github.com/emicklei/go-restful-openapi" "github.com/go-openapi/loads" "github.com/go-openapi/spec" "github.com/go-openapi/strfmt" "github.com/go-openapi/validate" "github.com/pkg/errors" - "io/ioutil" urlruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/klog" authoptions "kubesphere.io/kubesphere/pkg/apiserver/authentication/options" @@ -50,13 +52,13 @@ import ( tenantv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/tenant/v1alpha2" terminalv1alpha2 "kubesphere.io/kubesphere/pkg/kapis/terminal/v1alpha2" "kubesphere.io/kubesphere/pkg/models/iam/am" + "kubesphere.io/kubesphere/pkg/models/iam/group" "kubesphere.io/kubesphere/pkg/models/iam/im" fakedevops "kubesphere.io/kubesphere/pkg/simple/client/devops/fake" "kubesphere.io/kubesphere/pkg/simple/client/k8s" "kubesphere.io/kubesphere/pkg/simple/client/openpitrix" fakes3 "kubesphere.io/kubesphere/pkg/simple/client/s3/fake" "kubesphere.io/kubesphere/pkg/version" - "log" ) var output string @@ -119,7 +121,7 @@ func generateSwaggerJson() []byte { informerFactory.KubeSphereSharedInformerFactory(), "", "", "")) urlruntime.Must(devopsv1alpha2.AddToContainer(container, informerFactory.KubeSphereSharedInformerFactory(), &fakedevops.Devops{}, nil, clientsets.KubeSphere(), fakes3.NewFakeS3(), "")) urlruntime.Must(devopsv1alpha3.AddToContainer(container, &fakedevops.Devops{}, clientsets.Kubernetes(), clientsets.KubeSphere(), informerFactory.KubeSphereSharedInformerFactory(), informerFactory.KubernetesSharedInformerFactory())) - urlruntime.Must(iamv1alpha2.AddToContainer(container, im.NewOperator(clientsets.KubeSphere(), informerFactory, nil), am.NewReadOnlyOperator(informerFactory), authoptions.NewAuthenticateOptions())) + urlruntime.Must(iamv1alpha2.AddToContainer(container, im.NewOperator(clientsets.KubeSphere(), informerFactory, nil), am.NewReadOnlyOperator(informerFactory), group.New(informerFactory, clientsets.KubeSphere(), clientsets.Kubernetes()), authoptions.NewAuthenticateOptions())) urlruntime.Must(monitoringv1alpha3.AddToContainer(container, clientsets.Kubernetes(), nil, informerFactory, nil)) urlruntime.Must(openpitrixv1.AddToContainer(container, informerFactory, openpitrix.NewMockClient(nil))) urlruntime.Must(operationsv1alpha2.AddToContainer(container, clientsets.Kubernetes()))