diff --git a/cmd/controller-manager/app/controllers.go b/cmd/controller-manager/app/controllers.go index d3150f5ee..1d39370ba 100644 --- a/cmd/controller-manager/app/controllers.go +++ b/cmd/controller-manager/app/controllers.go @@ -122,7 +122,7 @@ func addControllers( client.KubeSphere(), devopsClient, informerFactory.KubernetesSharedInformerFactory().Core().V1().Namespaces(), informerFactory.KubeSphereSharedInformerFactory().Devops().V1alpha3().DevOpsProjects(), - ) + informerFactory.KubeSphereSharedInformerFactory().Tenant().V1alpha1().Workspaces()) devopsPipelineController = pipeline.NewController(client.Kubernetes(), client.KubeSphere(), diff --git a/pkg/apiserver/authorization/authorizer/interfaces.go b/pkg/apiserver/authorization/authorizer/interfaces.go index 10069b5b4..64e703b0b 100644 --- a/pkg/apiserver/authorization/authorizer/interfaces.go +++ b/pkg/apiserver/authorization/authorizer/interfaces.go @@ -51,6 +51,9 @@ type Attributes interface { // The namespace of the object, if a request is for a REST object. GetNamespace() string + // The devops project of the object, if a request is for a REST object. + GetDevOps() string + // The kind of object, if a request is for a REST object. GetResource() string @@ -109,6 +112,7 @@ type AttributesRecord struct { Cluster string Workspace string Namespace string + DevOps string APIGroup string APIVersion string Resource string @@ -144,6 +148,10 @@ func (a AttributesRecord) GetNamespace() string { return a.Namespace } +func (a AttributesRecord) GetDevOps() string { + return a.DevOps +} + func (a AttributesRecord) GetResource() string { return a.Resource } diff --git a/pkg/apiserver/authorization/authorizerfactory/rbac.go b/pkg/apiserver/authorization/authorizerfactory/rbac.go index a39cbaeb2..0d82340b4 100644 --- a/pkg/apiserver/authorization/authorizerfactory/rbac.go +++ b/pkg/apiserver/authorization/authorizerfactory/rbac.go @@ -234,11 +234,21 @@ func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes, } } - if requestAttributes.GetResourceScope() == request.WorkspaceScope || requestAttributes.GetResourceScope() == request.NamespaceScope { + if requestAttributes.GetResourceScope() == request.WorkspaceScope || + requestAttributes.GetResourceScope() == request.NamespaceScope || + requestAttributes.GetResourceScope() == request.DevOpsScope { + var workspace string var err error + // all of resource under namespace and devops belong to workspace if requestAttributes.GetResourceScope() == request.NamespaceScope { - if workspace, err = r.am.GetControlledWorkspace(requestAttributes.GetNamespace()); err != nil { + if workspace, err = r.am.GetNamespaceControlledWorkspace(requestAttributes.GetNamespace()); err != nil { + if !visitor(nil, "", nil, err) { + return + } + } + } else if requestAttributes.GetResourceScope() == request.DevOpsScope { + if workspace, err = r.am.GetDevOpsControlledWorkspace(requestAttributes.GetDevOps()); err != nil { if !visitor(nil, "", nil, err) { return } @@ -279,19 +289,33 @@ func (r *RBACAuthorizer) visitRulesFor(requestAttributes authorizer.Attributes, } } - if requestAttributes.GetResourceScope() == request.NamespaceScope { - if roleBindings, err := r.am.ListRoleBindings("", requestAttributes.GetNamespace()); err != nil { + if requestAttributes.GetResourceScope() == request.NamespaceScope || + requestAttributes.GetResourceScope() == request.DevOpsScope { + + namespace := requestAttributes.GetNamespace() + // list devops role binding + if requestAttributes.GetResourceScope() == request.DevOpsScope { + if relatedNamespace, err := r.am.GetDevOpsRelatedNamespace(requestAttributes.GetDevOps()); err != nil { + if !visitor(nil, "", nil, err) { + return + } + } else { + namespace = relatedNamespace + } + } + + if roleBindings, err := r.am.ListRoleBindings("", namespace); err != nil { if !visitor(nil, "", nil, err) { return } } else { sourceDescriber := &roleBindingDescriber{} for _, roleBinding := range roleBindings { - subjectIndex, applies := appliesTo(requestAttributes.GetUser(), roleBinding.Subjects, requestAttributes.GetNamespace()) + subjectIndex, applies := appliesTo(requestAttributes.GetUser(), roleBinding.Subjects, namespace) if !applies { continue } - regoPolicy, rules, err := r.am.GetRoleReferenceRules(roleBinding.RoleRef, requestAttributes.GetNamespace()) + regoPolicy, rules, err := r.am.GetRoleReferenceRules(roleBinding.RoleRef, namespace) if err != nil { visitor(nil, "", nil, err) continue diff --git a/pkg/apiserver/filters/authorization.go b/pkg/apiserver/filters/authorization.go index d241949cf..31346fd52 100644 --- a/pkg/apiserver/filters/authorization.go +++ b/pkg/apiserver/filters/authorization.go @@ -82,12 +82,12 @@ func getAuthorizerAttributes(ctx context.Context) (authorizer.Attributes, error) attribs.Cluster = requestInfo.Cluster attribs.Workspace = requestInfo.Workspace attribs.KubernetesRequest = requestInfo.IsKubernetesRequest - attribs.APIGroup = requestInfo.APIGroup attribs.APIVersion = requestInfo.APIVersion attribs.Resource = requestInfo.Resource attribs.Subresource = requestInfo.Subresource attribs.Namespace = requestInfo.Namespace + attribs.DevOps = requestInfo.DevOps attribs.Name = requestInfo.Name return &attribs, nil diff --git a/pkg/apiserver/request/requestinfo.go b/pkg/apiserver/request/requestinfo.go index 562a928b6..e5139341f 100644 --- a/pkg/apiserver/request/requestinfo.go +++ b/pkg/apiserver/request/requestinfo.go @@ -69,6 +69,9 @@ type RequestInfo struct { // Cluster of requested resource, this is empty in single-cluster environment Cluster string + // DevOps project of requested resource + DevOps string + // Scope of requested resource. ResourceScope string } @@ -195,15 +198,6 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er } } - selector := req.URL.Query().Get("labelSelector") - // URL forms: /api/v1/watch/namespaces?labelSelector=kubesphere.io/workspace=system-workspace - if strings.HasPrefix(selector, workspaceSelectorPrefix) { - workspace := strings.TrimPrefix(selector, workspaceSelectorPrefix) - // URL forms: /api/v1/watch/namespaces?labelSelector=kubesphere.io/workspace==system-workspace - workspace = strings.TrimPrefix(workspace, "=") - requestInfo.Workspace = workspace - } - // URL forms: /namespaces/{namespace}/{kind}/*, where parts are adjusted to be relative to kind if currentParts[0] == "namespaces" { if len(currentParts) > 1 { @@ -215,8 +209,19 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er currentParts = currentParts[2:] } } + } else if currentParts[0] == "devops" { + if len(currentParts) > 1 { + requestInfo.DevOps = currentParts[1] + + // if there is another step after the devops name + // move currentParts to include it as a resource in its own right + if len(currentParts) > 2 { + currentParts = currentParts[2:] + } + } } else { requestInfo.Namespace = metav1.NamespaceNone + requestInfo.DevOps = metav1.NamespaceNone } // parsing successful, so we now know the proper value for .Parts @@ -260,6 +265,15 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er requestInfo.Verb = "list" } + // URL forms: /api/v1/watch/namespaces?labelSelector=kubesphere.io/workspace=system-workspace + if requestInfo.Verb == "watch" { + selector := req.URL.Query().Get("labelSelector") + if strings.HasPrefix(selector, workspaceSelectorPrefix) { + workspace := strings.TrimPrefix(selector, workspaceSelectorPrefix) + requestInfo.Workspace = workspace + } + } + if opts.FieldSelector != nil { if name, ok := opts.FieldSelector.RequiresExactMatch("metadata.name"); ok { if len(path.IsValidPathSegmentName(name)) == 0 { @@ -306,6 +320,7 @@ const ( ClusterScope = "Cluster" WorkspaceScope = "Workspace" NamespaceScope = "Namespace" + DevOpsScope = "DevOps" workspaceSelectorPrefix = constants.WorkspaceLabelKey + "=" ) @@ -318,6 +333,10 @@ func (r *RequestInfoFactory) resolveResourceScope(request RequestInfo) string { return NamespaceScope } + if request.DevOps != "" { + return DevOpsScope + } + if request.Workspace != "" { return WorkspaceScope } diff --git a/pkg/controller/devopsproject/devopsproject_controller.go b/pkg/controller/devopsproject/devopsproject_controller.go index e7d2fd513..b08943de5 100644 --- a/pkg/controller/devopsproject/devopsproject_controller.go +++ b/pkg/controller/devopsproject/devopsproject_controller.go @@ -18,6 +18,8 @@ import ( "k8s.io/klog" devopsv1alpha3 "kubesphere.io/kubesphere/pkg/apis/devops/v1alpha3" "kubesphere.io/kubesphere/pkg/client/clientset/versioned/scheme" + tenantv1alpha1informers "kubesphere.io/kubesphere/pkg/client/informers/externalversions/tenant/v1alpha1" + tenantv1alpha1listers "kubesphere.io/kubesphere/pkg/client/listers/tenant/v1alpha1" "kubesphere.io/kubesphere/pkg/constants" devopsClient "kubesphere.io/kubesphere/pkg/simple/client/devops" "kubesphere.io/kubesphere/pkg/utils/k8sutil" @@ -48,6 +50,9 @@ type Controller struct { namespaceLister corev1lister.NamespaceLister namespaceSynced cache.InformerSynced + workspaceLister tenantv1alpha1listers.WorkspaceLister + workspaceSynced cache.InformerSynced + workqueue workqueue.RateLimitingInterface workerLoopPeriod time.Duration @@ -59,7 +64,8 @@ func NewController(client clientset.Interface, kubesphereClient kubesphereclient.Interface, devopsClinet devopsClient.Interface, namespaceInformer corev1informer.NamespaceInformer, - devopsInformer devopsinformers.DevOpsProjectInformer) *Controller { + devopsInformer devopsinformers.DevOpsProjectInformer, + workspaceInformer tenantv1alpha1informers.WorkspaceInformer) *Controller { broadcaster := record.NewBroadcaster() broadcaster.StartLogging(func(format string, args ...interface{}) { @@ -77,6 +83,8 @@ func NewController(client clientset.Interface, devOpsProjectSynced: devopsInformer.Informer().HasSynced, namespaceLister: namespaceInformer.Lister(), namespaceSynced: namespaceInformer.Informer().HasSynced, + workspaceLister: workspaceInformer.Lister(), + workspaceSynced: workspaceInformer.Informer().HasSynced, workerLoopPeriod: time.Second, } @@ -163,7 +171,7 @@ func (c *Controller) Run(workers int, stopCh <-chan struct{}) error { klog.Info("starting devops project controller") defer klog.Info("shutting down devops project controller") - if !cache.WaitForCacheSync(stopCh, c.devOpsProjectSynced) { + if !cache.WaitForCacheSync(stopCh, c.devOpsProjectSynced, c.devOpsProjectSynced, c.workspaceSynced) { return fmt.Errorf("failed to wait for caches to sync") } @@ -273,12 +281,18 @@ func (c *Controller) syncHandler(key string) error { } if !reflect.DeepEqual(copyProject, project) { - _, err := c.kubesphereClient.DevopsV1alpha3().DevOpsProjects().Update(copyProject) + copyProject, err = c.kubesphereClient.DevopsV1alpha3().DevOpsProjects().Update(copyProject) if err != nil { klog.V(8).Info(err, fmt.Sprintf("failed to update ns %s ", key)) return err } } + + if copyProject, err = c.bindWorkspace(copyProject); err != nil { + klog.Error(err) + return err + } + // Check project exists, otherwise we will create it. _, err := c.devopsClient.GetDevOpsProject(copyProject.Status.AdminNamespace) if err != nil { @@ -312,6 +326,38 @@ func (c *Controller) syncHandler(key string) error { return nil } +func (c *Controller) bindWorkspace(project *devopsv1alpha3.DevOpsProject) (*devopsv1alpha3.DevOpsProject, error) { + + workspaceName := project.Labels[constants.WorkspaceLabelKey] + + if workspaceName == "" { + return project, nil + } + + workspace, err := c.workspaceLister.Get(workspaceName) + + if err != nil { + // skip if workspace not found + if errors.IsNotFound(err) { + return project, nil + } + klog.Error(err) + return nil, err + } + + if !metav1.IsControlledBy(project, workspace) { + project.OwnerReferences = nil + if err := controllerutil.SetControllerReference(workspace, project, scheme.Scheme); err != nil { + klog.Error(err) + return nil, err + } + + return c.kubesphereClient.DevopsV1alpha3().DevOpsProjects().Update(project) + } + + return project, nil +} + func (c *Controller) deleteDevOpsProjectInDevOps(project *devopsv1alpha3.DevOpsProject) error { err := c.devopsClient.DeleteDevOpsProject(project.Status.AdminNamespace) @@ -330,9 +376,16 @@ func (c *Controller) generateNewNamespace(project *devopsv1alpha3.DevOpsProject) }, ObjectMeta: metav1.ObjectMeta{ GenerateName: project.Name, - Labels: map[string]string{constants.DevOpsProjectLabelKey: project.Name}, + Labels: map[string]string{ + constants.DevOpsProjectLabelKey: project.Name, + }, }, } + + if creator := project.Annotations[constants.CreatorAnnotationKey]; creator != "" { + ns.Annotations = map[string]string{constants.CreatorAnnotationKey: creator} + } + controllerutil.SetControllerReference(project, ns, scheme.Scheme) return ns } diff --git a/pkg/controller/devopsproject/devopsproject_controller_test.go b/pkg/controller/devopsproject/devopsproject_controller_test.go index 64ab7aa25..faec28cac 100644 --- a/pkg/controller/devopsproject/devopsproject_controller_test.go +++ b/pkg/controller/devopsproject/devopsproject_controller_test.go @@ -120,8 +120,10 @@ func (f *fixture) newController() (*Controller, informers.SharedInformerFactory, k8sI := kubeinformers.NewSharedInformerFactory(f.kubeclient, noResyncPeriodFunc()) dI := fakeDevOps.New(f.initDevOpsProject...) - c := NewController(f.kubeclient, f.client, dI, k8sI.Core().V1().Namespaces(), - i.Devops().V1alpha3().DevOpsProjects()) + c := NewController(f.kubeclient, f.client, dI, + k8sI.Core().V1().Namespaces(), + i.Devops().V1alpha3().DevOpsProjects(), + i.Tenant().V1alpha1().Workspaces()) c.devOpsProjectSynced = alwaysReady c.eventRecorder = &record.FakeRecorder{} @@ -251,7 +253,9 @@ func filterInformerActions(actions []core.Action) []core.Action { (action.Matches("list", devopsprojects.ResourcePluralDevOpsProject) || action.Matches("watch", devopsprojects.ResourcePluralDevOpsProject) || action.Matches("list", "namespaces") || - action.Matches("watch", "namespaces")) { + action.Matches("watch", "namespaces") || + action.Matches("watch", "workspaces") || + action.Matches("list", "workspaces")) { continue } ret = append(ret, action) diff --git a/pkg/controller/globalrolebinding/globalrolebinding_controller.go b/pkg/controller/globalrolebinding/globalrolebinding_controller.go index f7d80f4e2..97abc1e1c 100644 --- a/pkg/controller/globalrolebinding/globalrolebinding_controller.go +++ b/pkg/controller/globalrolebinding/globalrolebinding_controller.go @@ -229,17 +229,16 @@ func (c *Controller) reconcile(key string) error { } if globalRoleBinding.RoleRef.Name == iamv1alpha2.PlatformAdmin { - if err := c.relateToClusterAdmin(globalRoleBinding); err != nil { + if err := c.assignClusterAdminRole(globalRoleBinding); err != nil { klog.Error(err) return err } - if c.devopsClient != nil { - username := findExpectUsername(globalRoleBinding) - err = c.devopsClient.AssignGlobalRole(modeldevops.JenkinsAdminRoleName, username) - if err != nil { - klog.Errorf("%+v", err) - return err - } + } + + if c.devopsClient != nil { + if err := c.assignDevOpsAdminRole(globalRoleBinding); err != nil { + klog.Error(err) + return err } } @@ -299,11 +298,9 @@ func (c *Controller) multiClusterSync(globalRoleBinding *iamv1alpha2.GlobalRoleB return nil } -func (c *Controller) relateToClusterAdmin(globalRoleBinding *iamv1alpha2.GlobalRoleBinding) error { +func (c *Controller) assignClusterAdminRole(globalRoleBinding *iamv1alpha2.GlobalRoleBinding) error { username := findExpectUsername(globalRoleBinding) - - // unexpected if username == "" { return nil } @@ -436,6 +433,16 @@ func (c *Controller) ensureNotControlledByKubefed(globalRoleBinding *iamv1alpha2 return nil } +func (c *Controller) assignDevOpsAdminRole(globalRoleBinding *iamv1alpha2.GlobalRoleBinding) error { + if username := findExpectUsername(globalRoleBinding); username != "" { + if err := c.devopsClient.AssignGlobalRole(modeldevops.JenkinsAdminRoleName, username); err != nil { + klog.Errorf("%+v", err) + return err + } + } + return nil +} + func ensureSubjectAPIVersionIsValid(subjects []rbacv1.Subject) []rbacv1.Subject { validSubjects := make([]rbacv1.Subject, 0) for _, subject := range subjects { diff --git a/pkg/kapis/iam/v1alpha2/handler.go b/pkg/kapis/iam/v1alpha2/handler.go index 40b9b9db2..1790f238b 100644 --- a/pkg/kapis/iam/v1alpha2/handler.go +++ b/pkg/kapis/iam/v1alpha2/handler.go @@ -5,8 +5,6 @@ import ( "github.com/emicklei/go-restful" rbacv1 "k8s.io/api/rbac/v1" "k8s.io/apimachinery/pkg/api/errors" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/selection" "k8s.io/apiserver/pkg/authentication/user" "k8s.io/klog" "kubesphere.io/kubesphere/pkg/api" @@ -1079,7 +1077,7 @@ func (h *iamHandler) resolveNamespace(namespace string, devops string) (string, if devops == "" { return namespace, nil } - return h.am.GetControlledNamespace(devops) + return h.am.GetDevOpsRelatedNamespace(devops) } func (h *iamHandler) PatchWorkspaceRole(request *restful.Request, response *restful.Response) { @@ -1213,18 +1211,7 @@ func (h *iamHandler) updateGlobalRoleBinding(operator user.Info, user *iamv1alph func (h *iamHandler) ListUserLoginRecords(request *restful.Request, response *restful.Response) { username := request.PathParameter("user") queryParam := query.ParseQueryParameter(request) - selector, _ := labels.Parse(queryParam.LabelSelector) - if selector == nil { - selector = labels.NewSelector() - } - requirement, err := labels.NewRequirement(iamv1alpha2.UserReferenceLabel, selection.Equals, []string{username}) - if err != nil { - klog.Error(err) - handleError(request, response, err) - return - } - selector.Add(*requirement) - queryParam.LabelSelector = selector.String() + queryParam.Filters[query.FieldLabel] = query.Value(fmt.Sprintf("%s=%s", iamv1alpha2.UserReferenceLabel, username)) result, err := h.im.ListLoginRecords(queryParam) if err != nil { klog.Error(err) diff --git a/pkg/kapis/oauth/handler.go b/pkg/kapis/oauth/handler.go index df0cf825a..97cbd2193 100644 --- a/pkg/kapis/oauth/handler.go +++ b/pkg/kapis/oauth/handler.go @@ -290,6 +290,7 @@ func (h *handler) passwordGrant(username string, password string, req *restful.R return default: response.WriteError(http.StatusInternalServerError, apierrors.NewInternalError(err)) + return } } diff --git a/pkg/kapis/tenant/v1alpha2/handler.go b/pkg/kapis/tenant/v1alpha2/handler.go index 9ba583a92..d45a14c9f 100644 --- a/pkg/kapis/tenant/v1alpha2/handler.go +++ b/pkg/kapis/tenant/v1alpha2/handler.go @@ -101,6 +101,29 @@ func (h *tenantHandler) ListNamespaces(req *restful.Request, resp *restful.Respo resp.WriteEntity(result) } +func (h *tenantHandler) ListDevOpsProjects(req *restful.Request, resp *restful.Response) { + user, ok := request.UserFrom(req.Request.Context()) + queryParam := query.ParseQueryParameter(req) + + if !ok { + err := fmt.Errorf("cannot obtain user info") + klog.Errorln(err) + api.HandleForbidden(resp, nil, err) + return + } + + workspace := req.PathParameter("workspace") + + result, err := h.tenant.ListDevOpsProjects(user, workspace, queryParam) + + if err != nil { + api.HandleInternalError(resp, nil, err) + return + } + + resp.WriteEntity(result) +} + func (h *tenantHandler) CreateNamespace(request *restful.Request, response *restful.Response) { workspace := request.PathParameter("workspace") var namespace corev1.Namespace diff --git a/pkg/kapis/tenant/v1alpha2/register.go b/pkg/kapis/tenant/v1alpha2/register.go index 13ad037d3..956013e0a 100644 --- a/pkg/kapis/tenant/v1alpha2/register.go +++ b/pkg/kapis/tenant/v1alpha2/register.go @@ -27,7 +27,6 @@ import ( eventsv1alpha1 "kubesphere.io/kubesphere/pkg/api/events/v1alpha1" loggingv1alpha2 "kubesphere.io/kubesphere/pkg/api/logging/v1alpha2" tenantv1alpha2 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha2" - typesv1beta1 "kubesphere.io/kubesphere/pkg/apis/types/v1beta1" "kubesphere.io/kubesphere/pkg/apiserver/runtime" kubesphere "kubesphere.io/kubesphere/pkg/client/clientset/versioned" "kubesphere.io/kubesphere/pkg/constants" @@ -105,31 +104,37 @@ func AddToContainer(c *restful.Container, factory informers.InformerFactory, k8s To(handler.ListNamespaces). Param(ws.PathParameter("workspace", "workspace name")). Doc("List the namespaces for the current user"). - Returns(http.StatusOK, api.StatusOK, []corev1.Namespace{}). + Returns(http.StatusOK, api.StatusOK, api.ListResult{}). Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag})) ws.Route(ws.GET("/federatednamespaces"). To(handler.ListFederatedNamespaces). Param(ws.PathParameter("workspace", "workspace name")). Doc("List the federated namespaces for the current user"). - Returns(http.StatusOK, api.StatusOK, []typesv1beta1.FederatedNamespace{}). + Returns(http.StatusOK, api.StatusOK, api.ListResult{}). Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag})) ws.Route(ws.GET("/workspaces/{workspace}/federatednamespaces"). To(handler.ListFederatedNamespaces). Param(ws.PathParameter("workspace", "workspace name")). Doc("List the federated namespaces of the specified workspace for the current user"). - Returns(http.StatusOK, api.StatusOK, []typesv1beta1.FederatedNamespace{}). + Returns(http.StatusOK, api.StatusOK, api.ListResult{}). Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag})) ws.Route(ws.GET("/workspaces/{workspace}/namespaces"). To(handler.ListNamespaces). Param(ws.PathParameter("workspace", "workspace name")). Doc("List the namespaces of the specified workspace for the current user"). - Returns(http.StatusOK, api.StatusOK, []corev1.Namespace{}). + Returns(http.StatusOK, api.StatusOK, api.ListResult{}). + Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag})) + ws.Route(ws.GET("/workspaces/{workspace}/devops"). + To(handler.ListDevOpsProjects). + Param(ws.PathParameter("workspace", "workspace name")). + Doc("List the devops projects of the specified workspace for the current user"). + Returns(http.StatusOK, api.StatusOK, api.ListResult{}). Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag})) ws.Route(ws.GET("/workspaces/{workspace}/namespaces/{namespace}"). To(handler.DescribeNamespace). Param(ws.PathParameter("workspace", "workspace name")). Doc("Retrieve namespace details."). - Returns(http.StatusOK, api.StatusOK, []corev1.Namespace{}). + Returns(http.StatusOK, api.StatusOK, corev1.Namespace{}). Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag})) ws.Route(ws.DELETE("/workspaces/{workspace}/namespaces/{namespace}"). To(handler.DeleteNamespace). @@ -142,20 +147,20 @@ func AddToContainer(c *restful.Container, factory informers.InformerFactory, k8s Param(ws.PathParameter("workspace", "workspace name")). Doc("List the namespaces of the specified workspace for the current user"). Reads(corev1.Namespace{}). - Returns(http.StatusOK, api.StatusOK, []corev1.Namespace{}). + Returns(http.StatusOK, api.StatusOK, corev1.Namespace{}). Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag})) ws.Route(ws.PUT("/workspaces/{workspace}/namespaces/{namespace}"). To(handler.UpdateNamespace). Param(ws.PathParameter("workspace", "workspace name")). Reads(corev1.Namespace{}). - Returns(http.StatusOK, api.StatusOK, []corev1.Namespace{}). + Returns(http.StatusOK, api.StatusOK, corev1.Namespace{}). Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag})) ws.Route(ws.PATCH("/workspaces/{workspace}/namespaces/{namespace}"). To(handler.PatchNamespace). Consumes(mimePatch...). Param(ws.PathParameter("workspace", "workspace name")). Reads(corev1.Namespace{}). - Returns(http.StatusOK, api.StatusOK, []corev1.Namespace{}). + Returns(http.StatusOK, api.StatusOK, corev1.Namespace{}). Metadata(restfulspec.KeyOpenAPITags, []string{constants.TenantResourcesTag})) ws.Route(ws.GET("/events"). diff --git a/pkg/models/iam/am/am.go b/pkg/models/iam/am/am.go index 2c43baca1..76f93f695 100644 --- a/pkg/models/iam/am/am.go +++ b/pkg/models/iam/am/am.go @@ -70,8 +70,9 @@ type AccessManagementInterface interface { RemoveUserFromNamespace(username string, namespace string) error CreateClusterRoleBinding(username string, role string) error RemoveUserFromCluster(username string) error - GetControlledNamespace(devops string) (string, error) - GetControlledWorkspace(namespace string) (string, error) + GetDevOpsRelatedNamespace(devops string) (string, error) + GetNamespaceControlledWorkspace(namespace string) (string, error) + GetDevOpsControlledWorkspace(devops string) (string, error) PatchNamespaceRole(namespace string, role *rbacv1.Role) (*rbacv1.Role, error) PatchClusterRole(clusterRole *rbacv1.ClusterRole) (*rbacv1.ClusterRole, error) } @@ -279,23 +280,19 @@ func (am *amOperator) ListGlobalRoleBindings(username string) ([]*iamv1alpha2.Gl } func (am *amOperator) ListRoleBindings(username, namespace string) ([]*rbacv1.RoleBinding, error) { - roleBindings, err := am.resourceGetter.List(iamv1alpha2.ResourcesPluralRoleBinding, namespace, query.New()) - if err != nil { klog.Error(err) return nil, err } result := make([]*rbacv1.RoleBinding, 0) - for _, obj := range roleBindings.Items { roleBinding := obj.(*rbacv1.RoleBinding) if contains(roleBinding.Subjects, username) { result = append(result, roleBinding) } } - return result, nil } @@ -964,7 +961,7 @@ func (am *amOperator) GetClusterRole(name string) (*rbacv1.ClusterRole, error) { } return obj.(*rbacv1.ClusterRole), nil } -func (am *amOperator) GetControlledNamespace(devops string) (string, error) { +func (am *amOperator) GetDevOpsRelatedNamespace(devops string) (string, error) { obj, err := am.resourceGetter.Get(devopsv1alpha3.ResourcePluralDevOpsProject, "", devops) if err != nil { klog.Error(err) @@ -975,7 +972,17 @@ func (am *amOperator) GetControlledNamespace(devops string) (string, error) { return devopsProject.Status.AdminNamespace, nil } -func (am *amOperator) GetControlledWorkspace(namespace string) (string, error) { +func (am *amOperator) GetDevOpsControlledWorkspace(devops string) (string, error) { + obj, err := am.resourceGetter.Get(devopsv1alpha3.ResourcePluralDevOpsProject, "", devops) + if err != nil { + klog.Error(err) + return "", err + } + devopsProject := obj.(*devopsv1alpha3.DevOpsProject) + return devopsProject.Labels[tenantv1alpha1.WorkspaceLabel], nil +} + +func (am *amOperator) GetNamespaceControlledWorkspace(namespace string) (string, error) { obj, err := am.resourceGetter.Get("namespaces", "", namespace) if err != nil { if errors.IsNotFound(err) { diff --git a/pkg/models/tenant/devops.go b/pkg/models/tenant/devops.go index 6236e66a9..b65b5a4e9 100644 --- a/pkg/models/tenant/devops.go +++ b/pkg/models/tenant/devops.go @@ -17,26 +17,109 @@ limitations under the License. package tenant import ( - "kubesphere.io/kubesphere/pkg/models" - "kubesphere.io/kubesphere/pkg/server/params" - dsClient "kubesphere.io/kubesphere/pkg/simple/client/devops" + "fmt" + corev1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/klog" + "kubesphere.io/kubesphere/pkg/api" + devopsv1alpha3 "kubesphere.io/kubesphere/pkg/apis/devops/v1alpha3" + tenantv1alpha1 "kubesphere.io/kubesphere/pkg/apis/tenant/v1alpha1" + "kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer" + "kubesphere.io/kubesphere/pkg/apiserver/query" + "kubesphere.io/kubesphere/pkg/apiserver/request" + "kubesphere.io/kubesphere/pkg/constants" + resources "kubesphere.io/kubesphere/pkg/models/resources/v1alpha3" ) -type DevOpsProjectLister interface { - ListDevOpsProjects(workspace string, conditions *params.Conditions, orderBy string, reverse bool, limit int, offset int) (*models.PageableResponse, error) -} - -type devopsProjectLister struct { - dsProject dsClient.ProjectOperator -} - -func newProjectLister(client dsClient.ProjectOperator) DevOpsProjectLister { - return &devopsProjectLister{ - dsProject: client, +func (t *tenantOperator) ListDevOpsProjects(user user.Info, workspace string, queryParam *query.Query) (*api.ListResult, error) { + scope := request.ClusterScope + if workspace != "" { + scope = request.WorkspaceScope } -} -func (o *devopsProjectLister) ListDevOpsProjects(workspace string, conditions *params.Conditions, orderBy string, reverse bool, limit int, offset int) (*models.PageableResponse, error) { - //TODO: @runzexia use informer to impl it - return nil, nil + listDevOps := authorizer.AttributesRecord{ + User: user, + Verb: "list", + Workspace: workspace, + Resource: "devops", + ResourceRequest: true, + ResourceScope: scope, + } + + decision, _, err := t.authorizer.Authorize(listDevOps) + if err != nil { + klog.Error(err) + return nil, err + } + + // allowed list devops in the specified scope + if decision == authorizer.DecisionAllow { + // filter by workspace + if workspace != "" { + queryParam.Filters[query.FieldLabel] = query.Value(fmt.Sprintf("%s=%s", tenantv1alpha1.WorkspaceLabel, workspace)) + } + result, err := t.resourceGetter.List(devopsv1alpha3.ResourcePluralDevOpsProject, "", queryParam) + if err != nil { + klog.Error(err) + return nil, err + } + return result, nil + } + + roleBindings, err := t.am.ListRoleBindings(user.GetName(), "") + if err != nil { + klog.Error(err) + return nil, err + } + + devopsProjects := make([]runtime.Object, 0) + for _, roleBinding := range roleBindings { + obj, err := t.resourceGetter.Get("namespaces", "", roleBinding.Namespace) + if err != nil { + klog.Error(err) + return nil, err + } + namespace := obj.(*corev1.Namespace) + controlledDevOpsProject := namespace.Labels[constants.DevOpsProjectLabelKey] + // skip if not controlled by devops project + if controlledDevOpsProject == "" { + continue + } + + devopsProject, err := t.resourceGetter.Get(devopsv1alpha3.ResourcePluralDevOpsProject, "", controlledDevOpsProject) + if err != nil { + if errors.IsNotFound(err) { + klog.Warning("orphan devops project found ", devopsProject) + continue + } + klog.Error(err) + return nil, err + } + + // skip if not controlled by the specified workspace + if workspace != "" && + devopsProject.(*devopsv1alpha3.DevOpsProject).Labels[tenantv1alpha1.WorkspaceLabel] != workspace { + continue + } + + if !contains(devopsProjects, devopsProject) { + devopsProjects = append(devopsProjects, namespace) + } + } + + result := resources.DefaultList(devopsProjects, queryParam, func(left runtime.Object, right runtime.Object, field query.Field) bool { + return resources.DefaultObjectMetaCompare(left.(*devopsv1alpha3.DevOpsProject).ObjectMeta, right.(*devopsv1alpha3.DevOpsProject).ObjectMeta, field) + }, func(object runtime.Object, filter query.Filter) bool { + devopsProject := object.(*devopsv1alpha3.DevOpsProject).ObjectMeta + if workspace != "" { + if workspaceLabel, ok := devopsProject.Labels[tenantv1alpha1.WorkspaceLabel]; !ok || workspaceLabel != workspace { + return false + } + } + return resources.DefaultObjectMetaFilter(devopsProject, filter) + }) + + return result, nil } diff --git a/pkg/models/tenant/tenant.go b/pkg/models/tenant/tenant.go index 8b7c18274..b8bbb27a8 100644 --- a/pkg/models/tenant/tenant.go +++ b/pkg/models/tenant/tenant.go @@ -60,6 +60,7 @@ import ( type Interface interface { ListWorkspaces(user user.Info, query *query.Query) (*api.ListResult, 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) ListFederatedNamespaces(info user.Info, workspace string, param *query.Query) (*api.ListResult, error) CreateNamespace(workspace string, namespace *corev1.Namespace) (*corev1.Namespace, error) CreateWorkspace(workspace *tenantv1alpha2.WorkspaceTemplate) (*tenantv1alpha2.WorkspaceTemplate, error)