From 40b3ddc142b906a79f22c726a0a61dd113c39485 Mon Sep 17 00:00:00 2001 From: Igor Drozdov Date: Tue, 7 Apr 2020 07:00:04 +0300 Subject: Extract customaction into a separate module We'll reuse this module for uploadpack in the future --- internal/command/receivepack/customaction.go | 88 ------------------ internal/command/receivepack/customaction_test.go | 103 --------------------- internal/command/receivepack/receivepack.go | 4 +- internal/command/receivepack/receivepack_test.go | 22 ++++- .../command/shared/customaction/customaction.go | 95 +++++++++++++++++++ .../shared/customaction/customaction_test.go | 85 +++++++++++++++++ .../testhelper/requesthandlers/requesthandlers.go | 40 ++++++++ 7 files changed, 241 insertions(+), 196 deletions(-) delete mode 100644 internal/command/receivepack/customaction.go delete mode 100644 internal/command/receivepack/customaction_test.go create mode 100644 internal/command/shared/customaction/customaction.go create mode 100644 internal/command/shared/customaction/customaction_test.go diff --git a/internal/command/receivepack/customaction.go b/internal/command/receivepack/customaction.go deleted file mode 100644 index 6693d23..0000000 --- a/internal/command/receivepack/customaction.go +++ /dev/null @@ -1,88 +0,0 @@ -package receivepack - -import ( - "bytes" - "errors" - - "io" - "io/ioutil" - "net/http" - - "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet" - "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/accessverifier" -) - -type Request struct { - SecretToken []byte `json:"secret_token"` - Data accessverifier.CustomPayloadData `json:"data"` - Output []byte `json:"output"` -} - -type Response struct { - Result []byte `json:"result"` - Message string `json:"message"` -} - -func (c *Command) processCustomAction(response *accessverifier.Response) error { - data := response.Payload.Data - apiEndpoints := data.ApiEndpoints - - if len(apiEndpoints) == 0 { - return errors.New("Custom action error: Empty API endpoints") - } - - return c.processApiEndpoints(response) -} - -func (c *Command) processApiEndpoints(response *accessverifier.Response) error { - client, err := gitlabnet.GetClient(c.Config) - - if err != nil { - return err - } - - data := response.Payload.Data - request := &Request{Data: data} - request.Data.UserId = response.Who - - for _, endpoint := range data.ApiEndpoints { - response, err := c.performRequest(client, endpoint, request) - if err != nil { - return err - } - - if err = c.displayResult(response.Result); err != nil { - return err - } - - // In the context of the git push sequence of events, it's necessary to read - // stdin in order to capture output to pass onto subsequent commands - output, err := ioutil.ReadAll(c.ReadWriter.In) - if err != nil { - return err - } - request.Output = output - } - - return nil -} - -func (c *Command) performRequest(client *gitlabnet.GitlabClient, endpoint string, request *Request) (*Response, error) { - response, err := client.DoRequest(http.MethodPost, endpoint, request) - if err != nil { - return nil, err - } - defer response.Body.Close() - - cr := &Response{} - if err := gitlabnet.ParseJSON(response, cr); err != nil { - return nil, err - } - - return cr, nil -} - -func (c *Command) displayResult(result []byte) error { - _, err := io.Copy(c.ReadWriter.Out, bytes.NewReader(result)) - return err -} diff --git a/internal/command/receivepack/customaction_test.go b/internal/command/receivepack/customaction_test.go deleted file mode 100644 index c55a8f3..0000000 --- a/internal/command/receivepack/customaction_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package receivepack - -import ( - "bytes" - "encoding/json" - "io/ioutil" - "net/http" - "testing" - - "github.com/stretchr/testify/require" - - "gitlab.com/gitlab-org/gitlab-shell/internal/command/commandargs" - "gitlab.com/gitlab-org/gitlab-shell/internal/command/readwriter" - "gitlab.com/gitlab-org/gitlab-shell/internal/config" - "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/accessverifier" - "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/testserver" -) - -func TestCustomReceivePack(t *testing.T) { - repo := "group/repo" - keyId := "1" - - requests := []testserver.TestRequestHandler{ - { - Path: "/api/v4/internal/allowed", - Handler: func(w http.ResponseWriter, r *http.Request) { - b, err := ioutil.ReadAll(r.Body) - require.NoError(t, err) - - var request *accessverifier.Request - require.NoError(t, json.Unmarshal(b, &request)) - - require.Equal(t, "1", request.KeyId) - - body := map[string]interface{}{ - "status": true, - "gl_id": "1", - "payload": map[string]interface{}{ - "action": "geo_proxy_to_primary", - "data": map[string]interface{}{ - "api_endpoints": []string{"/geo/proxy_git_push_ssh/info_refs", "/geo/proxy_git_push_ssh/push"}, - "gl_username": "custom", - "primary_repo": "https://repo/path", - }, - }, - } - w.WriteHeader(http.StatusMultipleChoices) - require.NoError(t, json.NewEncoder(w).Encode(body)) - }, - }, - { - Path: "/geo/proxy_git_push_ssh/info_refs", - Handler: func(w http.ResponseWriter, r *http.Request) { - b, err := ioutil.ReadAll(r.Body) - require.NoError(t, err) - - var request *Request - require.NoError(t, json.Unmarshal(b, &request)) - - require.Equal(t, request.Data.UserId, "key-"+keyId) - require.Empty(t, request.Output) - - err = json.NewEncoder(w).Encode(Response{Result: []byte("custom")}) - require.NoError(t, err) - }, - }, - { - Path: "/geo/proxy_git_push_ssh/push", - Handler: func(w http.ResponseWriter, r *http.Request) { - b, err := ioutil.ReadAll(r.Body) - require.NoError(t, err) - - var request *Request - require.NoError(t, json.Unmarshal(b, &request)) - - require.Equal(t, request.Data.UserId, "key-"+keyId) - require.Equal(t, "input", string(request.Output)) - - err = json.NewEncoder(w).Encode(Response{Result: []byte("output")}) - require.NoError(t, err) - }, - }, - } - - url, cleanup := testserver.StartSocketHttpServer(t, requests) - defer cleanup() - - outBuf := &bytes.Buffer{} - errBuf := &bytes.Buffer{} - input := bytes.NewBufferString("input") - - cmd := &Command{ - Config: &config.Config{GitlabUrl: url}, - Args: &commandargs.Shell{GitlabKeyId: keyId, CommandType: commandargs.ReceivePack, SshArgs: []string{"git-receive-pack", repo}}, - ReadWriter: &readwriter.ReadWriter{ErrOut: errBuf, Out: outBuf, In: input}, - } - - require.NoError(t, cmd.Execute()) - - // expect printing of info message, "custom" string from the first request - // and "output" string from the second request - require.Equal(t, "customoutput", outBuf.String()) -} diff --git a/internal/command/receivepack/receivepack.go b/internal/command/receivepack/receivepack.go index aaaf7b0..3af3941 100644 --- a/internal/command/receivepack/receivepack.go +++ b/internal/command/receivepack/receivepack.go @@ -4,6 +4,7 @@ import ( "gitlab.com/gitlab-org/gitlab-shell/internal/command/commandargs" "gitlab.com/gitlab-org/gitlab-shell/internal/command/readwriter" "gitlab.com/gitlab-org/gitlab-shell/internal/command/shared/accessverifier" + "gitlab.com/gitlab-org/gitlab-shell/internal/command/shared/customaction" "gitlab.com/gitlab-org/gitlab-shell/internal/command/shared/disallowedcommand" "gitlab.com/gitlab-org/gitlab-shell/internal/config" ) @@ -27,7 +28,8 @@ func (c *Command) Execute() error { } if response.IsCustomAction() { - return c.processCustomAction(response) + customAction := customaction.Command{c.Config, c.ReadWriter} + return customAction.Execute(response) } return c.performGitalyCall(response) diff --git a/internal/command/receivepack/receivepack_test.go b/internal/command/receivepack/receivepack_test.go index 1d7bd21..d464e35 100644 --- a/internal/command/receivepack/receivepack_test.go +++ b/internal/command/receivepack/receivepack_test.go @@ -15,18 +15,32 @@ import ( func TestForbiddenAccess(t *testing.T) { requests := requesthandlers.BuildDisallowedByApiHandlers(t) - url, cleanup := testserver.StartHttpServer(t, requests) + cmd, _, cleanup := setup(t, "disallowed", requests) defer cleanup() + err := cmd.Execute() + require.Equal(t, "Disallowed by API call", err.Error()) +} + +func TestCustomReceivePack(t *testing.T) { + cmd, output, cleanup := setup(t, "1", requesthandlers.BuildAllowedWithCustomActionsHandlers(t)) + defer cleanup() + + require.NoError(t, cmd.Execute()) + require.Equal(t, "customoutput", output.String()) +} + +func setup(t *testing.T, keyId string, requests []testserver.TestRequestHandler) (*Command, *bytes.Buffer, func()) { + url, cleanup := testserver.StartSocketHttpServer(t, requests) + output := &bytes.Buffer{} input := bytes.NewBufferString("input") cmd := &Command{ Config: &config.Config{GitlabUrl: url}, - Args: &commandargs.Shell{GitlabKeyId: "disallowed", SshArgs: []string{"git-receive-pack", "group/repo"}}, + Args: &commandargs.Shell{GitlabKeyId: keyId, SshArgs: []string{"git-receive-pack", "group/repo"}}, ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output, In: input}, } - err := cmd.Execute() - require.Equal(t, "Disallowed by API call", err.Error()) + return cmd, output, cleanup } diff --git a/internal/command/shared/customaction/customaction.go b/internal/command/shared/customaction/customaction.go new file mode 100644 index 0000000..c4b6647 --- /dev/null +++ b/internal/command/shared/customaction/customaction.go @@ -0,0 +1,95 @@ +package customaction + +import ( + "bytes" + "errors" + + "io" + "io/ioutil" + "net/http" + + "gitlab.com/gitlab-org/gitlab-shell/internal/command/readwriter" + "gitlab.com/gitlab-org/gitlab-shell/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet" + "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/accessverifier" +) + +type Request struct { + SecretToken []byte `json:"secret_token"` + Data accessverifier.CustomPayloadData `json:"data"` + Output []byte `json:"output"` +} + +type Response struct { + Result []byte `json:"result"` + Message string `json:"message"` +} + +type Command struct { + Config *config.Config + ReadWriter *readwriter.ReadWriter +} + +func (c *Command) Execute(response *accessverifier.Response) error { + data := response.Payload.Data + apiEndpoints := data.ApiEndpoints + + if len(apiEndpoints) == 0 { + return errors.New("Custom action error: Empty API endpoints") + } + + return c.processApiEndpoints(response) +} + +func (c *Command) processApiEndpoints(response *accessverifier.Response) error { + client, err := gitlabnet.GetClient(c.Config) + + if err != nil { + return err + } + + data := response.Payload.Data + request := &Request{Data: data} + request.Data.UserId = response.Who + + for _, endpoint := range data.ApiEndpoints { + response, err := c.performRequest(client, endpoint, request) + if err != nil { + return err + } + + if err = c.displayResult(response.Result); err != nil { + return err + } + + // In the context of the git push sequence of events, it's necessary to read + // stdin in order to capture output to pass onto subsequent commands + output, err := ioutil.ReadAll(c.ReadWriter.In) + if err != nil { + return err + } + request.Output = output + } + + return nil +} + +func (c *Command) performRequest(client *gitlabnet.GitlabClient, endpoint string, request *Request) (*Response, error) { + response, err := client.DoRequest(http.MethodPost, endpoint, request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + cr := &Response{} + if err := gitlabnet.ParseJSON(response, cr); err != nil { + return nil, err + } + + return cr, nil +} + +func (c *Command) displayResult(result []byte) error { + _, err := io.Copy(c.ReadWriter.Out, bytes.NewReader(result)) + return err +} diff --git a/internal/command/shared/customaction/customaction_test.go b/internal/command/shared/customaction/customaction_test.go new file mode 100644 index 0000000..3dfe288 --- /dev/null +++ b/internal/command/shared/customaction/customaction_test.go @@ -0,0 +1,85 @@ +package customaction + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-shell/internal/command/readwriter" + "gitlab.com/gitlab-org/gitlab-shell/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/accessverifier" + "gitlab.com/gitlab-org/gitlab-shell/internal/gitlabnet/testserver" +) + +func TestExecute(t *testing.T) { + who := "key-1" + + requests := []testserver.TestRequestHandler{ + { + Path: "/geo/proxy/info_refs", + Handler: func(w http.ResponseWriter, r *http.Request) { + b, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + + var request *Request + require.NoError(t, json.Unmarshal(b, &request)) + + require.Equal(t, request.Data.UserId, who) + require.Empty(t, request.Output) + + err = json.NewEncoder(w).Encode(Response{Result: []byte("custom")}) + require.NoError(t, err) + }, + }, + { + Path: "/geo/proxy/push", + Handler: func(w http.ResponseWriter, r *http.Request) { + b, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + + var request *Request + require.NoError(t, json.Unmarshal(b, &request)) + + require.Equal(t, request.Data.UserId, who) + require.Equal(t, "input", string(request.Output)) + + err = json.NewEncoder(w).Encode(Response{Result: []byte("output")}) + require.NoError(t, err) + }, + }, + } + + url, cleanup := testserver.StartSocketHttpServer(t, requests) + defer cleanup() + + outBuf := &bytes.Buffer{} + errBuf := &bytes.Buffer{} + input := bytes.NewBufferString("input") + + response := &accessverifier.Response{ + Who: who, + Payload: accessverifier.CustomPayload{ + Action: "geo_proxy_to_primary", + Data: accessverifier.CustomPayloadData{ + ApiEndpoints: []string{"/geo/proxy/info_refs", "/geo/proxy/push"}, + Username: "custom", + PrimaryRepo: "https://repo/path", + }, + }, + } + + cmd := &Command{ + Config: &config.Config{GitlabUrl: url}, + ReadWriter: &readwriter.ReadWriter{ErrOut: errBuf, Out: outBuf, In: input}, + } + + require.NoError(t, cmd.Execute(response)) + + // expect printing of info message, "custom" string from the first request + // and "output" string from the second request + require.Equal(t, "customoutput", outBuf.String()) +} diff --git a/internal/testhelper/requesthandlers/requesthandlers.go b/internal/testhelper/requesthandlers/requesthandlers.go index fef53b6..75827fa 100644 --- a/internal/testhelper/requesthandlers/requesthandlers.go +++ b/internal/testhelper/requesthandlers/requesthandlers.go @@ -61,3 +61,43 @@ func BuildAllowedWithGitalyHandlers(t *testing.T, gitalyAddress string) []testse return requests } + +func BuildAllowedWithCustomActionsHandlers(t *testing.T) []testserver.TestRequestHandler { + requests := []testserver.TestRequestHandler{ + { + Path: "/api/v4/internal/allowed", + Handler: func(w http.ResponseWriter, r *http.Request) { + body := map[string]interface{}{ + "status": true, + "gl_id": "1", + "payload": map[string]interface{}{ + "action": "geo_proxy_to_primary", + "data": map[string]interface{}{ + "api_endpoints": []string{"/geo/proxy/info_refs", "/geo/proxy/push"}, + "gl_username": "custom", + "primary_repo": "https://repo/path", + }, + }, + } + w.WriteHeader(http.StatusMultipleChoices) + require.NoError(t, json.NewEncoder(w).Encode(body)) + }, + }, + { + Path: "/geo/proxy/info_refs", + Handler: func(w http.ResponseWriter, r *http.Request) { + body := map[string]interface{}{"result": []byte("custom")} + require.NoError(t, json.NewEncoder(w).Encode(body)) + }, + }, + { + Path: "/geo/proxy/push", + Handler: func(w http.ResponseWriter, r *http.Request) { + body := map[string]interface{}{"result": []byte("output")} + require.NoError(t, json.NewEncoder(w).Encode(body)) + }, + }, + } + + return requests +} -- cgit v1.2.1