diff --git a/hack/docker_build.sh b/hack/docker_build.sh index 59b0e91d2..404d5120a 100755 --- a/hack/docker_build.sh +++ b/hack/docker_build.sh @@ -17,8 +17,16 @@ tag_for_branch() { } get_repo() { - local repo=$1 + local repo=${REPO} # read from env repo=${repo:-kubespheredev} + if [[ "$1" != "" ]]; then + repo="$1" + fi + + # set the default value if there's no user defined + if [[ "${repo}" == "" ]]; then + repo="kubespheredev" + fi echo "$repo" } diff --git a/pkg/apiserver/apiserver.go b/pkg/apiserver/apiserver.go index 20fa0ce93..a989c2c67 100644 --- a/pkg/apiserver/apiserver.go +++ b/pkg/apiserver/apiserver.go @@ -250,7 +250,7 @@ func (s *APIServer) installKubeSphereAPIs() { s.KubernetesClient.KubeSphere(), s.S3Client, s.Config.DevopsOptions.Host, - amOperator)) + rbacAuthorizer)) urlruntime.Must(devopsv1alpha3.AddToContainer(s.container, s.DevopsClient, s.KubernetesClient.Kubernetes(), diff --git a/pkg/apiserver/authorization/authorizer/interfaces.go b/pkg/apiserver/authorization/authorizer/interfaces.go index 64e703b0b..181b9cb5c 100644 --- a/pkg/apiserver/authorization/authorizer/interfaces.go +++ b/pkg/apiserver/authorization/authorizer/interfaces.go @@ -133,7 +133,7 @@ func (a AttributesRecord) GetVerb() string { } func (a AttributesRecord) IsReadOnly() bool { - return a.Verb == "get" || a.Verb == "list" || a.Verb == "watch" + return a.Verb == VerbGet || a.Verb == VerbList || a.Verb == VerbWatch } func (a AttributesRecord) GetCluster() string { @@ -199,3 +199,14 @@ const ( // to allow or deny an action. DecisionNoOpinion ) + +const ( + // VerbList represents the verb of listing resources + VerbList = "list" + // VerbCreate represents the verb of creating a resource + VerbCreate = "create" + // VerbGet represents the verb of getting a resource or resources + VerbGet = "get" + // VerbWatch represents the verb of watching a resource + VerbWatch = "watch" +) diff --git a/pkg/kapis/devops/v1alpha2/devops.go b/pkg/kapis/devops/v1alpha2/devops.go index fa8a8f510..503924290 100644 --- a/pkg/kapis/devops/v1alpha2/devops.go +++ b/pkg/kapis/devops/v1alpha2/devops.go @@ -23,9 +23,10 @@ import ( "github.com/emicklei/go-restful" "k8s.io/apiserver/pkg/authentication/user" log "k8s.io/klog" + "k8s.io/klog/v2" "kubesphere.io/kubesphere/pkg/api" "kubesphere.io/kubesphere/pkg/apis/devops/v1alpha3" - iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2" + "kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer" "kubesphere.io/kubesphere/pkg/apiserver/request" "kubesphere.io/kubesphere/pkg/constants" "kubesphere.io/kubesphere/pkg/models/devops" @@ -277,11 +278,53 @@ func (h *ProjectPipelineHandler) GetPipelineRunNodes(req *restful.Request, resp resp.WriteAsJson(res) } -func (h *ProjectPipelineHandler) approvableCheck(nodes []clientDevOps.NodesDetail, req *restful.Request) { - currentUserName, roleName := h.getCurrentUser(req) +// there're two situation here: +// 1. the particular submitters exist +// the users who are the owner of this Pipeline or the submitter of this Pipeline, or has the auth to create a DevOps project +// 2. no particular submitters +// only the owner of this Pipeline can approve or reject it +func (h *ProjectPipelineHandler) approvableCheck(nodes []clientDevOps.NodesDetail, pipe pipelineParam) { + var userInfo user.Info + var ok bool + var isAdmin bool // check if current user belong to the admin group, grant it if it's true - isAdmin := roleName == iamv1alpha2.PlatformAdmin + if userInfo, ok = request.UserFrom(pipe.Context); ok { + createAuth := authorizer.AttributesRecord{ + User: userInfo, + Verb: authorizer.VerbCreate, + Workspace: pipe.Workspace, + Resource: "devopsprojects", + ResourceRequest: true, + ResourceScope: request.DevOpsScope, + } + if decision, _, err := h.authorizer.Authorize(createAuth); err == nil { + isAdmin = decision == authorizer.DecisionAllow + } else { + // this is an expected case, printing the debug info for troubleshooting + klog.V(8).Infof("authorize failed with '%v', error is '%v'", + createAuth, err) + } + } else { + klog.V(6).Infof("cannot get the current user when checking the approvable with pipeline '%s/%s'", + pipe.ProjectName, pipe.Name) + return + } + + var createdByCurrentUser bool // indicate if the current user is the owner + if pipeline, err := h.devopsOperator.GetPipelineObj(pipe.ProjectName, pipe.Name); err == nil { + if creator, ok := pipeline.GetAnnotations()[constants.CreatorAnnotationKey]; ok { + createdByCurrentUser = userInfo.GetName() == creator + } else { + klog.V(6).Infof("annotation '%s' is necessary but it is missing from '%s/%s'", + constants.CreatorAnnotationKey, pipe.ProjectName, pipe.Name) + } + } else { + klog.V(6).Infof("cannot find pipeline '%s/%s', error is '%v'", pipe.ProjectName, pipe.Name, err) + return + } + + // check every input steps if it's approvable for i, node := range nodes { if node.State != clientDevOps.StatePaused { continue @@ -292,7 +335,7 @@ func (h *ProjectPipelineHandler) approvableCheck(nodes []clientDevOps.NodesDetai continue } - nodes[i].Steps[j].Approvable = isAdmin || step.Input.Approvable(currentUserName) + nodes[i].Steps[j].Approvable = isAdmin || createdByCurrentUser || step.Input.Approvable(userInfo.GetName()) } } } @@ -308,33 +351,8 @@ func (h *ProjectPipelineHandler) createdBy(projectName string, pipelineName stri return false } -func (h *ProjectPipelineHandler) getCurrentUser(req *restful.Request) (username, roleName string) { - var userInfo user.Info - var ok bool - var err error - - ctx := req.Request.Context() - if userInfo, ok = request.UserFrom(ctx); ok { - var role *iamv1alpha2.GlobalRole - username = userInfo.GetName() - if role, err = h.amInterface.GetGlobalRoleOfUser(username); err == nil { - roleName = role.Name - } - } - return -} - func (h *ProjectPipelineHandler) hasSubmitPermission(req *restful.Request) (hasPermit bool, err error) { - currentUserName, roleName := h.getCurrentUser(req) - projectName := req.PathParameter("devops") - pipelineName := req.PathParameter("pipeline") - // check if current user belong to the admin group or he's the owner, grant it if it's true - if roleName == iamv1alpha2.PlatformAdmin || h.createdBy(projectName, pipelineName, currentUserName) { - hasPermit = true - return - } - - // step 2, check if current user if was addressed + pipeParam := parsePipelineParam(req) httpReq := &http.Request{ URL: req.Request.URL, Header: req.Request.Header, @@ -348,7 +366,9 @@ func (h *ProjectPipelineHandler) hasSubmitPermission(req *restful.Request) (hasP // check if current user can approve this input var res []clientDevOps.NodesDetail - if res, err = h.devopsOperator.GetNodesDetail(projectName, pipelineName, runId, httpReq); err == nil { + if res, err = h.devopsOperator.GetNodesDetail(pipeParam.ProjectName, pipeParam.Name, runId, httpReq); err == nil { + h.approvableCheck(res, parsePipelineParam(req)) + for _, node := range res { if node.ID != nodeId { continue @@ -359,7 +379,7 @@ func (h *ProjectPipelineHandler) hasSubmitPermission(req *restful.Request) (hasP continue } - hasPermit = step.Input.Approvable(currentUserName) + hasPermit = step.Approvable break } break @@ -410,7 +430,7 @@ func (h *ProjectPipelineHandler) GetNodesDetail(req *restful.Request, resp *rest parseErr(err, resp) return } - h.approvableCheck(res, req) + h.approvableCheck(res, parsePipelineParam(req)) resp.WriteAsJson(res) } @@ -618,10 +638,19 @@ func (h *ProjectPipelineHandler) GetBranchNodesDetail(req *restful.Request, resp parseErr(err, resp) return } - h.approvableCheck(res, req) + h.approvableCheck(res, parsePipelineParam(req)) resp.WriteAsJson(res) } +func parsePipelineParam(req *restful.Request) pipelineParam { + return pipelineParam{ + Workspace: req.PathParameter("workspace"), + ProjectName: req.PathParameter("devops"), + Name: req.PathParameter("pipeline"), + Context: req.Request.Context(), + } +} + func (h *ProjectPipelineHandler) GetPipelineBranch(req *restful.Request, resp *restful.Response) { projectName := req.PathParameter("devops") pipelineName := req.PathParameter("pipeline") diff --git a/pkg/kapis/devops/v1alpha2/handler.go b/pkg/kapis/devops/v1alpha2/handler.go index 56ea6147f..586648902 100644 --- a/pkg/kapis/devops/v1alpha2/handler.go +++ b/pkg/kapis/devops/v1alpha2/handler.go @@ -17,10 +17,10 @@ limitations under the License. package v1alpha2 import ( + "kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer" "kubesphere.io/kubesphere/pkg/client/clientset/versioned" "kubesphere.io/kubesphere/pkg/client/informers/externalversions" "kubesphere.io/kubesphere/pkg/models/devops" - "kubesphere.io/kubesphere/pkg/models/iam/am" devopsClient "kubesphere.io/kubesphere/pkg/simple/client/devops" "kubesphere.io/kubesphere/pkg/simple/client/s3" "kubesphere.io/kubesphere/pkg/simple/client/sonarqube" @@ -29,18 +29,19 @@ import ( type ProjectPipelineHandler struct { devopsOperator devops.DevopsOperator projectCredentialGetter devops.ProjectCredentialGetter - amInterface am.AccessManagementInterface + authorizer authorizer.Authorizer } type PipelineSonarHandler struct { pipelineSonarGetter devops.PipelineSonarGetter } -func NewProjectPipelineHandler(devopsClient devopsClient.Interface, ksInformers externalversions.SharedInformerFactory, amInterface am.AccessManagementInterface) ProjectPipelineHandler { +func NewProjectPipelineHandler(devopsClient devopsClient.Interface, ksInformers externalversions.SharedInformerFactory, + authorizer authorizer.Authorizer) ProjectPipelineHandler { return ProjectPipelineHandler{ devopsOperator: devops.NewDevopsOperator(devopsClient, nil, nil, ksInformers, nil), projectCredentialGetter: devops.NewProjectCredentialOperator(devopsClient), - amInterface: amInterface, + authorizer: authorizer, } } diff --git a/pkg/kapis/devops/v1alpha2/register.go b/pkg/kapis/devops/v1alpha2/register.go index 3f7f1dd21..0fcbd3bc9 100644 --- a/pkg/kapis/devops/v1alpha2/register.go +++ b/pkg/kapis/devops/v1alpha2/register.go @@ -17,6 +17,7 @@ limitations under the License. package v1alpha2 import ( + "context" "fmt" "github.com/emicklei/go-restful" "github.com/emicklei/go-restful-openapi" @@ -24,18 +25,17 @@ import ( "k8s.io/apimachinery/pkg/util/proxy" "k8s.io/klog" devopsv1alpha1 "kubesphere.io/kubesphere/pkg/apis/devops/v1alpha1" + "kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer" "kubesphere.io/kubesphere/pkg/apiserver/runtime" "kubesphere.io/kubesphere/pkg/client/clientset/versioned" "kubesphere.io/kubesphere/pkg/client/informers/externalversions" "kubesphere.io/kubesphere/pkg/constants" - "kubesphere.io/kubesphere/pkg/models/iam/am" "kubesphere.io/kubesphere/pkg/simple/client/devops/jenkins" "kubesphere.io/kubesphere/pkg/simple/client/s3" "kubesphere.io/kubesphere/pkg/simple/client/sonarqube" "net/url" "strings" - //"kubesphere.io/kubesphere/pkg/models/devops" "kubesphere.io/kubesphere/pkg/simple/client/devops" "net/http" ) @@ -47,10 +47,12 @@ const ( var GroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha2"} -func AddToContainer(container *restful.Container, ksInformers externalversions.SharedInformerFactory, devopsClient devops.Interface, sonarqubeClient sonarqube.SonarInterface, ksClient versioned.Interface, s3Client s3.Interface, endpoint string, amInterface am.AccessManagementInterface) error { +func AddToContainer(container *restful.Container, ksInformers externalversions.SharedInformerFactory, devopsClient devops.Interface, + sonarqubeClient sonarqube.SonarInterface, ksClient versioned.Interface, s3Client s3.Interface, endpoint string, + authorizer authorizer.Authorizer) error { ws := runtime.NewWebService(GroupVersion) - err := AddPipelineToWebService(ws, devopsClient, ksInformers, amInterface) + err := AddPipelineToWebService(ws, devopsClient, ksInformers, authorizer) if err != nil { return err } @@ -75,12 +77,13 @@ func AddToContainer(container *restful.Container, ksInformers externalversions.S return nil } -func AddPipelineToWebService(webservice *restful.WebService, devopsClient devops.Interface, ksInformers externalversions.SharedInformerFactory, amInterface am.AccessManagementInterface) error { +func AddPipelineToWebService(webservice *restful.WebService, devopsClient devops.Interface, ksInformers externalversions.SharedInformerFactory, + authorizer authorizer.Authorizer) error { projectPipelineEnable := devopsClient != nil if projectPipelineEnable { - projectPipelineHandler := NewProjectPipelineHandler(devopsClient, ksInformers, amInterface) + projectPipelineHandler := NewProjectPipelineHandler(devopsClient, ksInformers, authorizer) webservice.Route(webservice.GET("/devops/{devops}/credentials/{credential}/usage"). To(projectPipelineHandler.GetProjectCredentialUsage). @@ -723,6 +726,14 @@ func AddJenkinsToContainer(webservice *restful.WebService, devopsClient devops.I return nil } +type pipelineParam struct { + Workspace string + ProjectName string + Name string + + Context context.Context +} + type errorResponder struct{} func (e *errorResponder) Error(w http.ResponseWriter, req *http.Request, err error) { diff --git a/pkg/simple/client/devops/pipeline.go b/pkg/simple/client/devops/pipeline.go index 3f0ed64bd..0260db4bf 100644 --- a/pkg/simple/client/devops/pipeline.go +++ b/pkg/simple/client/devops/pipeline.go @@ -1122,14 +1122,10 @@ func (i *Input) GetSubmitters() (submitters []string) { func (i *Input) Approvable(identify string) (ok bool) { submitters := i.GetSubmitters() - // it means anyone can approve this if there's no specific one - if len(submitters) == 0 { - ok = true - } else { - for _, submitter := range submitters { - if submitter == identify { - ok = true - } + for _, submitter := range submitters { + if submitter == identify { + ok = true + break } } return diff --git a/pkg/simple/client/devops/pipeline_test.go b/pkg/simple/client/devops/pipeline_test.go index 2f9bb0ed6..db60a4f75 100644 --- a/pkg/simple/client/devops/pipeline_test.go +++ b/pkg/simple/client/devops/pipeline_test.go @@ -19,8 +19,8 @@ func TestGetSubmitters(t *testing.T) { func TestApprovable(t *testing.T) { input := &Input{} - assert.Equal(t, input.Approvable(""), true, "should allow anyone to approve it if there's no submitter given") - assert.Equal(t, input.Approvable("fake"), true, "should allow anyone to approve it if there's no submitter given") + assert.Equal(t, input.Approvable(""), false, "should allow anyone to approve it if there's no submitter given") + assert.Equal(t, input.Approvable("fake"), false, "should allow anyone to approve it if there's no submitter given") input.Submitter = "fake" assert.Equal(t, input.Approvable(""), false, "should not approve by nobody if there's a particular submitter")