Merge pull request #3156 from LinuxSuRen/pipeline-approve

Restrict only specific users or admin can approve a pipeline
This commit is contained in:
KubeSphere CI Bot
2020-12-08 17:49:43 +08:00
committed by GitHub
9 changed files with 230 additions and 24 deletions

1
go.mod
View File

@@ -81,6 +81,7 @@ require (
gopkg.in/src-d/go-git.v4 v4.11.0
gopkg.in/yaml.v2 v2.3.0
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c
gotest.tools v2.2.0+incompatible
istio.io/api v0.0.0-20191111210003-35e06ef8d838
istio.io/client-go v0.0.0-20191113122552-9bd0ba57c3d2
k8s.io/api v0.17.5

View File

@@ -210,7 +210,8 @@ func (s *APIServer) installKubeSphereAPIs() {
s.SonarClient,
s.KubernetesClient.KubeSphere(),
s.S3Client,
s.Config.DevopsOptions.Host))
s.Config.DevopsOptions.Host,
am.NewOperator(s.InformerFactory, s.KubernetesClient.KubeSphere(), s.KubernetesClient.Kubernetes())))
urlruntime.Must(devopsv1alpha3.AddToContainer(s.container,
s.DevopsClient,
s.KubernetesClient.Kubernetes(),

View File

@@ -17,10 +17,18 @@ limitations under the License.
package v1alpha2
import (
"encoding/json"
"errors"
"fmt"
"github.com/emicklei/go-restful"
"k8s.io/apiserver/pkg/authentication/user"
log "k8s.io/klog"
"kubesphere.io/kubesphere/pkg/api"
iamv1alpha2 "kubesphere.io/kubesphere/pkg/apis/iam/v1alpha2"
"kubesphere.io/kubesphere/pkg/apiserver/request"
"kubesphere.io/kubesphere/pkg/constants"
"kubesphere.io/kubesphere/pkg/models/devops"
clientDevOps "kubesphere.io/kubesphere/pkg/simple/client/devops"
"net/http"
"strings"
)
@@ -202,6 +210,101 @@ 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)
// check if current user belong to the admin group, grant it if it's true
isAdmin := roleName == iamv1alpha2.PlatformAdmin
for i, node := range nodes {
if node.State != clientDevOps.StatePaused {
continue
}
for j, step := range node.Steps {
if step.State != clientDevOps.StatePaused || step.Input == nil {
continue
}
nodes[i].Steps[j].Approvable = isAdmin || step.Input.Approvable(currentUserName)
}
}
}
func (h *ProjectPipelineHandler) createdBy(projectName string, pipelineName string, currentUserName string) bool {
if pipeline, err := h.devopsOperator.GetPipelineObj(projectName, pipelineName); err == nil {
if creator, ok := pipeline.Annotations[constants.CreatorAnnotationKey]; ok {
return creator == currentUserName
}
} else {
log.V(4).Infof("cannot get pipeline %s/%s, error %#v", projectName, pipelineName, err)
}
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
httpReq := &http.Request{
URL: req.Request.URL,
Header: req.Request.Header,
Form: req.Request.Form,
PostForm: req.Request.PostForm,
}
runId := req.PathParameter("run")
nodeId := req.PathParameter("node")
stepId := req.PathParameter("step")
// check if current user can approve this input
var res []clientDevOps.NodesDetail
if res, err = h.devopsOperator.GetNodesDetail(projectName, pipelineName, runId, httpReq); err == nil {
for _, node := range res {
if node.ID != nodeId {
continue
}
for _, step := range node.Steps {
if step.ID != stepId || step.Input == nil {
continue
}
hasPermit = step.Input.Approvable(currentUserName)
break
}
break
}
} else {
log.V(4).Infof("cannot get nodes detail, error: %v", err)
err = errors.New("cannot get the submitters of current pipeline run")
return
}
return
}
func (h *ProjectPipelineHandler) SubmitInputStep(req *restful.Request, resp *restful.Response) {
projectName := req.PathParameter("devops")
pipelineName := req.PathParameter("pipeline")
@@ -209,13 +312,25 @@ func (h *ProjectPipelineHandler) SubmitInputStep(req *restful.Request, resp *res
nodeId := req.PathParameter("node")
stepId := req.PathParameter("step")
res, err := h.devopsOperator.SubmitInputStep(projectName, pipelineName, runId, nodeId, stepId, req.Request)
if err != nil {
parseErr(err, resp)
return
}
var response []byte
var err error
var ok bool
resp.Write(res)
if ok, err = h.hasSubmitPermission(req); !ok || err != nil {
msg := map[string]string{
"allow": "false",
"message": fmt.Sprintf("%v", err),
}
response, _ = json.Marshal(msg)
} else {
response, err = h.devopsOperator.SubmitInputStep(projectName, pipelineName, runId, nodeId, stepId, req.Request)
if err != nil {
parseErr(err, resp)
return
}
}
resp.Write(response)
}
func (h *ProjectPipelineHandler) GetNodesDetail(req *restful.Request, resp *restful.Response) {
@@ -228,6 +343,8 @@ func (h *ProjectPipelineHandler) GetNodesDetail(req *restful.Request, resp *rest
parseErr(err, resp)
return
}
h.approvableCheck(res, req)
resp.WriteAsJson(res)
}
@@ -401,13 +518,26 @@ func (h *ProjectPipelineHandler) SubmitBranchInputStep(req *restful.Request, res
nodeId := req.PathParameter("node")
stepId := req.PathParameter("step")
res, err := h.devopsOperator.SubmitBranchInputStep(projectName, pipelineName, branchName, runId, nodeId, stepId, req.Request)
if err != nil {
parseErr(err, resp)
return
var response []byte
var err error
var ok bool
if ok, err = h.hasSubmitPermission(req); !ok || err != nil {
msg := map[string]string{
"allow": "false",
"message": fmt.Sprintf("%v", err),
}
response, _ = json.Marshal(msg)
} else {
response, err = h.devopsOperator.SubmitBranchInputStep(projectName, pipelineName, branchName, runId, nodeId, stepId, req.Request)
if err != nil {
parseErr(err, resp)
return
}
}
resp.Write(res)
resp.Write(response)
}
func (h *ProjectPipelineHandler) GetBranchNodesDetail(req *restful.Request, resp *restful.Response) {
@@ -421,6 +551,7 @@ func (h *ProjectPipelineHandler) GetBranchNodesDetail(req *restful.Request, resp
parseErr(err, resp)
return
}
h.approvableCheck(res, req)
resp.WriteAsJson(res)
}

View File

@@ -20,6 +20,7 @@ import (
"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"
@@ -28,16 +29,18 @@ import (
type ProjectPipelineHandler struct {
devopsOperator devops.DevopsOperator
projectCredentialGetter devops.ProjectCredentialGetter
amInterface am.AccessManagementInterface
}
type PipelineSonarHandler struct {
pipelineSonarGetter devops.PipelineSonarGetter
}
func NewProjectPipelineHandler(devopsClient devopsClient.Interface) ProjectPipelineHandler {
func NewProjectPipelineHandler(devopsClient devopsClient.Interface, ksInformers externalversions.SharedInformerFactory, amInterface am.AccessManagementInterface) ProjectPipelineHandler {
return ProjectPipelineHandler{
devopsOperator: devops.NewDevopsOperator(devopsClient, nil, nil, nil, nil),
devopsOperator: devops.NewDevopsOperator(devopsClient, nil, nil, ksInformers, nil),
projectCredentialGetter: devops.NewProjectCredentialOperator(devopsClient),
amInterface: amInterface,
}
}

View File

@@ -28,6 +28,7 @@ import (
"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"
@@ -46,10 +47,10 @@ 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) error {
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 {
ws := runtime.NewWebService(GroupVersion)
err := AddPipelineToWebService(ws, devopsClient)
err := AddPipelineToWebService(ws, devopsClient, ksInformers, amInterface)
if err != nil {
return err
}
@@ -74,12 +75,12 @@ func AddToContainer(container *restful.Container, ksInformers externalversions.S
return nil
}
func AddPipelineToWebService(webservice *restful.WebService, devopsClient devops.Interface) error {
func AddPipelineToWebService(webservice *restful.WebService, devopsClient devops.Interface, ksInformers externalversions.SharedInformerFactory, amInterface am.AccessManagementInterface) error {
projectPipelineEnable := devopsClient != nil
if projectPipelineEnable {
projectPipelineHandler := NewProjectPipelineHandler(devopsClient)
projectPipelineHandler := NewProjectPipelineHandler(devopsClient, ksInformers, amInterface)
webservice.Route(webservice.GET("/devops/{devops}/credentials/{credential}/usage").
To(projectPipelineHandler.GetProjectCredentialUsage).

View File

@@ -487,20 +487,16 @@ func (d devopsOperator) GetNodeSteps(projectName, pipelineName, runId, nodeId st
}
func (d devopsOperator) GetPipelineRunNodes(projectName, pipelineName, runId string, req *http.Request) ([]devops.PipelineRunNodes, error) {
res, err := d.devopsClient.GetPipelineRunNodes(projectName, pipelineName, runId, convertToHttpParameters(req))
if err != nil {
klog.Error(err)
return nil, err
}
fmt.Println()
return res, err
}
func (d devopsOperator) SubmitInputStep(projectName, pipelineName, runId, nodeId, stepId string, req *http.Request) ([]byte, error) {
newBody, err := getInputReqBody(req.Body)
if err != nil {
klog.Error(err)

View File

@@ -17,9 +17,11 @@ limitations under the License.
package devops
import (
"fmt"
"io"
"net/http"
"net/url"
"strings"
)
type PipelineList struct {
@@ -979,6 +981,8 @@ type NodeSteps struct {
StartTime string `json:"startTime,omitempty" description:"the time of starts"`
State string `json:"state,omitempty" description:"run state. e.g. SKIPPED"`
Type string `json:"type,omitempty" description:"type"`
// Approvable indicates if this step can be approved by current user
Approvable bool `json:"aprovable" description:"indicate if this step can be approved by current user"`
}
// CheckScriptCompile
@@ -1075,6 +1079,11 @@ type NodesDetail struct {
Steps []NodeSteps `json:"steps,omitempty" description:"steps"`
}
const (
// StatePaused indicates a node or a step was paused, for example it's waiting for an iput
StatePaused = "PAUSED"
)
type NodesStepsIndex struct {
Id int `json:"id,omitempty" description:"id"`
Steps []NodeSteps `json:"steps,omitempty" description:"steps"`
@@ -1095,6 +1104,37 @@ type Input struct {
Submitter interface{} `json:"submitter,omitempty" description:"check submitter"`
}
// GetSubmitters returns the all submitters related to this input
func (i *Input) GetSubmitters() (submitters []string) {
if i.Submitter == nil {
return
}
submitterArray := strings.Split(fmt.Sprintf("%v", i.Submitter), ",")
submitters = make([]string, len(submitterArray))
for i, submitter := range submitterArray {
submitters[i] = strings.TrimSpace(submitter)
}
return
}
// Approvable returns the result if the given identify (username or group name) can approve this input
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
}
}
}
return
}
type HttpParameters struct {
Method string `json:"method,omitempty"`
Header http.Header `json:"header,omitempty"`
@@ -1105,7 +1145,6 @@ type HttpParameters struct {
}
type PipelineOperator interface {
// Pipelinne operator interface
GetPipeline(projectName, pipelineName string, httpParameters *HttpParameters) (*Pipeline, error)
ListPipelines(httpParameters *HttpParameters) (*PipelineList, error)

View File

@@ -0,0 +1,34 @@
package devops
import (
"gotest.tools/assert"
"testing"
)
func TestGetSubmitters(t *testing.T) {
input := &Input{}
assert.Equal(t, len(input.GetSubmitters()), 0,
"errors happen when try to get submitters without any submitters")
input.Submitter = "a , b, c,d"
submitters := input.GetSubmitters()
assert.Equal(t, len(submitters), 4, "get incorrect number of submitters")
assert.DeepEqual(t, submitters, []string{"a", "b", "c", "d"})
}
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")
input.Submitter = "fake"
assert.Equal(t, input.Approvable(""), false, "should not approve by nobody if there's a particular submitter")
assert.Equal(t, input.Approvable("rick"), false, "should not approve by who is not the specific one")
assert.Equal(t, input.Approvable("fake"), true, "should be approvable")
input.Submitter = "fake, good ,bad"
assert.Equal(t, input.Approvable("fake"), true, "should be approvable")
assert.Equal(t, input.Approvable("good"), true, "should be approvable")
assert.Equal(t, input.Approvable("bad"), true, "should be approvable")
}

View File

@@ -119,7 +119,7 @@ func generateSwaggerJson() []byte {
urlruntime.Must(oauth.AddToContainer(container, nil, nil, nil, nil, nil))
urlruntime.Must(clusterkapisv1alpha1.AddToContainer(container, informerFactory.KubernetesSharedInformerFactory(),
informerFactory.KubeSphereSharedInformerFactory(), "", "", ""))
urlruntime.Must(devopsv1alpha2.AddToContainer(container, informerFactory.KubeSphereSharedInformerFactory(), &fakedevops.Devops{}, nil, clientsets.KubeSphere(), fakes3.NewFakeS3(), ""))
urlruntime.Must(devopsv1alpha2.AddToContainer(container, informerFactory.KubeSphereSharedInformerFactory(), &fakedevops.Devops{}, nil, clientsets.KubeSphere(), fakes3.NewFakeS3(), "", am.NewReadOnlyOperator(informerFactory)))
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), group.New(informerFactory, clientsets.KubeSphere(), clientsets.Kubernetes()), authoptions.NewAuthenticateOptions()))
urlruntime.Must(monitoringv1alpha3.AddToContainer(container, clientsets.Kubernetes(), nil, informerFactory, nil))