diff options
Diffstat (limited to 'go/internal/gitlabnet')
-rw-r--r-- | go/internal/gitlabnet/accessverifier/client.go | 119 | ||||
-rw-r--r-- | go/internal/gitlabnet/accessverifier/client_test.go | 208 | ||||
-rw-r--r-- | go/internal/gitlabnet/client.go | 10 | ||||
-rw-r--r-- | go/internal/gitlabnet/client_test.go | 6 | ||||
-rw-r--r-- | go/internal/gitlabnet/discover/client_test.go | 3 | ||||
-rw-r--r-- | go/internal/gitlabnet/httpclient_test.go | 3 | ||||
-rw-r--r-- | go/internal/gitlabnet/httpsclient_test.go | 3 | ||||
-rw-r--r-- | go/internal/gitlabnet/testserver/gitalyserver.go | 63 | ||||
-rw-r--r-- | go/internal/gitlabnet/testserver/testserver.go | 29 | ||||
-rw-r--r-- | go/internal/gitlabnet/twofactorrecover/client_test.go | 3 |
10 files changed, 414 insertions, 33 deletions
diff --git a/go/internal/gitlabnet/accessverifier/client.go b/go/internal/gitlabnet/accessverifier/client.go new file mode 100644 index 0000000..ebe8545 --- /dev/null +++ b/go/internal/gitlabnet/accessverifier/client.go @@ -0,0 +1,119 @@ +package accessverifier + +import ( + "fmt" + "net/http" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" +) + +const ( + protocol = "ssh" + anyChanges = "_any" +) + +type Client struct { + client *gitlabnet.GitlabClient +} + +type Request struct { + Action commandargs.CommandType `json:"action"` + Repo string `json:"project"` + Changes string `json:"changes"` + Protocol string `json:"protocol"` + KeyId string `json:"key_id,omitempty"` + Username string `json:"username,omitempty"` +} + +type GitalyRepo struct { + StorageName string `json:"storage_name"` + RelativePath string `json:"relative_path"` + GitObjectDirectory string `json:"git_object_directory"` + GitAlternateObjectDirectories []string `json:"git_alternate_object_directories"` + RepoName string `json:"gl_repository"` + ProjectPath string `json:"gl_project_path"` +} + +type Gitaly struct { + Repo GitalyRepo `json:"repository"` + Address string `json:"address"` + Token string `json:"token"` +} + +type CustomPayloadData struct { + ApiEndpoints []string `json:"api_endpoints"` + Username string `json:"gl_username"` + PrimaryRepo string `json:"primary_repo"` + InfoMessage string `json:"info_message"` + UserId string `json:"gl_id,omitempty"` +} + +type CustomPayload struct { + Action string `json:"action"` + Data CustomPayloadData `json:"data"` +} + +type Response struct { + Success bool `json:"status"` + Message string `json:"message"` + Repo string `json:"gl_repository"` + UserId string `json:"gl_id"` + Username string `json:"gl_username"` + GitConfigOptions []string `json:"git_config_options"` + Gitaly Gitaly `json:"gitaly"` + GitProtocol string `json:"git_protocol"` + Payload CustomPayload `json:"payload"` + ConsoleMessages []string `json:"gl_console_messages"` + Who string + StatusCode int +} + +func NewClient(config *config.Config) (*Client, error) { + client, err := gitlabnet.GetClient(config) + if err != nil { + return nil, fmt.Errorf("Error creating http client: %v", err) + } + + return &Client{client: client}, nil +} + +func (c *Client) Verify(args *commandargs.CommandArgs, action commandargs.CommandType, repo string) (*Response, error) { + request := &Request{Action: action, Repo: repo, Protocol: protocol, Changes: anyChanges} + + if args.GitlabUsername != "" { + request.Username = args.GitlabUsername + } else { + request.KeyId = args.GitlabKeyId + } + + response, err := c.client.Post("/allowed", request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + return parse(response, args) +} + +func parse(hr *http.Response, args *commandargs.CommandArgs) (*Response, error) { + response := &Response{} + if err := gitlabnet.ParseJSON(hr, response); err != nil { + return nil, err + } + + if args.GitlabKeyId != "" { + response.Who = "key-" + args.GitlabKeyId + } else { + response.Who = response.UserId + } + + response.StatusCode = hr.StatusCode + + return response, nil +} + +func (r *Response) IsCustomAction() bool { + return r.StatusCode == http.StatusMultipleChoices +} diff --git a/go/internal/gitlabnet/accessverifier/client_test.go b/go/internal/gitlabnet/accessverifier/client_test.go new file mode 100644 index 0000000..a759919 --- /dev/null +++ b/go/internal/gitlabnet/accessverifier/client_test.go @@ -0,0 +1,208 @@ +package accessverifier + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "path" + "testing" + + "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" +) + +var ( + repo = "group/private" + action = commandargs.ReceivePack +) + +func buildExpectedResponse(who string) *Response { + response := &Response{ + Success: true, + UserId: "user-1", + Repo: "project-26", + Username: "root", + GitConfigOptions: []string{"option"}, + Gitaly: Gitaly{ + Repo: GitalyRepo{ + StorageName: "default", + RelativePath: "@hashed/5f/9c/5f9c4ab08cac7457e9111a30e4664920607ea2c115a1433d7be98e97e64244ca.git", + GitObjectDirectory: "path/to/git_object_directory", + GitAlternateObjectDirectories: []string{"path/to/git_alternate_object_directory"}, + RepoName: "project-26", + ProjectPath: repo, + }, + Address: "unix:gitaly.socket", + Token: "token", + }, + GitProtocol: "protocol", + Payload: CustomPayload{}, + ConsoleMessages: []string{"console", "message"}, + Who: who, + StatusCode: 200, + } + + return response +} + +func TestSuccessfulResponses(t *testing.T) { + client, cleanup := setup(t) + defer cleanup() + + testCases := []struct { + desc string + args *commandargs.CommandArgs + who string + }{ + { + desc: "Provide key id within the request", + args: &commandargs.CommandArgs{GitlabKeyId: "1"}, + who: "key-1", + }, { + desc: "Provide username within the request", + args: &commandargs.CommandArgs{GitlabUsername: "first"}, + who: "user-1", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + result, err := client.Verify(tc.args, action, repo) + require.NoError(t, err) + + response := buildExpectedResponse(tc.who) + require.Equal(t, response, result) + }) + } +} + +func TestGetCustomAction(t *testing.T) { + client, cleanup := setup(t) + defer cleanup() + + args := &commandargs.CommandArgs{GitlabUsername: "custom"} + result, err := client.Verify(args, action, repo) + require.NoError(t, err) + + response := buildExpectedResponse("user-1") + response.Payload = CustomPayload{ + Action: "geo_proxy_to_primary", + Data: CustomPayloadData{ + ApiEndpoints: []string{"geo/proxy_git_push_ssh/info_refs", "geo/proxy_git_push_ssh/push"}, + Username: "custom", + PrimaryRepo: "https://repo/path", + InfoMessage: "message", + }, + } + response.StatusCode = 300 + + require.True(t, response.IsCustomAction()) + require.Equal(t, response, result) +} + +func TestErrorResponses(t *testing.T) { + client, cleanup := setup(t) + defer cleanup() + + testCases := []struct { + desc string + fakeId string + expectedError string + }{ + { + desc: "A response with an error message", + fakeId: "2", + expectedError: "Not allowed!", + }, + { + desc: "A response with bad JSON", + fakeId: "3", + expectedError: "Parsing failed", + }, + { + desc: "An error response without message", + fakeId: "4", + expectedError: "Internal API error (403)", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + args := &commandargs.CommandArgs{GitlabKeyId: tc.fakeId} + resp, err := client.Verify(args, action, repo) + + require.EqualError(t, err, tc.expectedError) + require.Nil(t, resp) + }) + } +} + +func setup(t *testing.T) (*Client, func()) { + testDirCleanup, err := testhelper.PrepareTestRootDir() + require.NoError(t, err) + defer testDirCleanup() + + body, err := ioutil.ReadFile(path.Join(testhelper.TestRoot, "responses/allowed.json")) + require.NoError(t, err) + + allowedWithPayloadPath := path.Join(testhelper.TestRoot, "responses/allowed_with_payload.json") + bodyWithPayload, err := ioutil.ReadFile(allowedWithPayloadPath) + require.NoError(t, err) + + 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 requestBody *Request + require.NoError(t, json.Unmarshal(b, &requestBody)) + + switch requestBody.Username { + case "first": + _, err = w.Write(body) + require.NoError(t, err) + case "second": + errBody := map[string]interface{}{ + "status": false, + "message": "missing user", + } + require.NoError(t, json.NewEncoder(w).Encode(errBody)) + case "custom": + w.WriteHeader(http.StatusMultipleChoices) + _, err = w.Write(bodyWithPayload) + require.NoError(t, err) + } + + switch requestBody.KeyId { + case "1": + _, err = w.Write(body) + require.NoError(t, err) + case "2": + w.WriteHeader(http.StatusForbidden) + errBody := &gitlabnet.ErrorResponse{ + Message: "Not allowed!", + } + require.NoError(t, json.NewEncoder(w).Encode(errBody)) + case "3": + w.Write([]byte("{ \"message\": \"broken json!\"")) + case "4": + w.WriteHeader(http.StatusForbidden) + } + }, + }, + } + + url, cleanup := testserver.StartSocketHttpServer(t, requests) + + client, err := NewClient(&config.Config{GitlabUrl: url}) + require.NoError(t, err) + + return client, cleanup +} diff --git a/go/internal/gitlabnet/client.go b/go/internal/gitlabnet/client.go index 86add04..dacb1d6 100644 --- a/go/internal/gitlabnet/client.go +++ b/go/internal/gitlabnet/client.go @@ -53,8 +53,6 @@ func normalizePath(path string) string { } func newRequest(method, host, path string, data interface{}) (*http.Request, error) { - path = normalizePath(path) - var jsonReader io.Reader if data != nil { jsonData, err := json.Marshal(data) @@ -74,7 +72,7 @@ func newRequest(method, host, path string, data interface{}) (*http.Request, err } func parseError(resp *http.Response) error { - if resp.StatusCode >= 200 && resp.StatusCode <= 299 { + if resp.StatusCode >= 200 && resp.StatusCode <= 399 { return nil } defer resp.Body.Close() @@ -89,14 +87,14 @@ func parseError(resp *http.Response) error { } func (c *GitlabClient) Get(path string) (*http.Response, error) { - return c.doRequest("GET", path, nil) + return c.DoRequest(http.MethodGet, normalizePath(path), nil) } func (c *GitlabClient) Post(path string, data interface{}) (*http.Response, error) { - return c.doRequest("POST", path, data) + return c.DoRequest(http.MethodPost, normalizePath(path), data) } -func (c *GitlabClient) doRequest(method, path string, data interface{}) (*http.Response, error) { +func (c *GitlabClient) DoRequest(method, path string, data interface{}) (*http.Response, error) { request, err := newRequest(method, c.host, path, data) if err != nil { return nil, err diff --git a/go/internal/gitlabnet/client_test.go b/go/internal/gitlabnet/client_test.go index d817239..cf42195 100644 --- a/go/internal/gitlabnet/client_test.go +++ b/go/internal/gitlabnet/client_test.go @@ -11,6 +11,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" @@ -71,7 +72,7 @@ func TestClients(t *testing.T) { testCases := []struct { desc string config *config.Config - server func([]testserver.TestRequestHandler) (func(), string, error) + server func(*testing.T, []testserver.TestRequestHandler) (string, func()) }{ { desc: "Socket client", @@ -94,9 +95,8 @@ func TestClients(t *testing.T) { for _, tc := range testCases { t.Run(tc.desc, func(t *testing.T) { - cleanup, url, err := tc.server(requests) + url, cleanup := tc.server(t, requests) defer cleanup() - require.NoError(t, err) tc.config.GitlabUrl = url tc.config.Secret = "sssh, it's a secret" diff --git a/go/internal/gitlabnet/discover/client_test.go b/go/internal/gitlabnet/discover/client_test.go index 8eabdd7..b98a28e 100644 --- a/go/internal/gitlabnet/discover/client_test.go +++ b/go/internal/gitlabnet/discover/client_test.go @@ -128,8 +128,7 @@ func TestErrorResponses(t *testing.T) { } func setup(t *testing.T) (*Client, func()) { - cleanup, url, err := testserver.StartSocketHttpServer(requests) - require.NoError(t, err) + url, cleanup := testserver.StartSocketHttpServer(t, requests) client, err := NewClient(&config.Config{GitlabUrl: url}) require.NoError(t, err) diff --git a/go/internal/gitlabnet/httpclient_test.go b/go/internal/gitlabnet/httpclient_test.go index 885a6d1..9b635bd 100644 --- a/go/internal/gitlabnet/httpclient_test.go +++ b/go/internal/gitlabnet/httpclient_test.go @@ -86,8 +86,7 @@ func TestEmptyBasicAuthSettings(t *testing.T) { } func setup(t *testing.T, config *config.Config, requests []testserver.TestRequestHandler) (*GitlabClient, func()) { - cleanup, url, err := testserver.StartHttpServer(requests) - require.NoError(t, err) + url, cleanup := testserver.StartHttpServer(t, requests) config.GitlabUrl = url client, err := GetClient(config) diff --git a/go/internal/gitlabnet/httpsclient_test.go b/go/internal/gitlabnet/httpsclient_test.go index b9baad8..04901df 100644 --- a/go/internal/gitlabnet/httpsclient_test.go +++ b/go/internal/gitlabnet/httpsclient_test.go @@ -115,8 +115,7 @@ func setupWithRequests(t *testing.T, config *config.Config) (*GitlabClient, func }, } - cleanup, url, err := testserver.StartHttpsServer(requests) - require.NoError(t, err) + url, cleanup := testserver.StartHttpsServer(t, requests) config.GitlabUrl = url client, err := GetClient(config) diff --git a/go/internal/gitlabnet/testserver/gitalyserver.go b/go/internal/gitlabnet/testserver/gitalyserver.go new file mode 100644 index 0000000..141a518 --- /dev/null +++ b/go/internal/gitlabnet/testserver/gitalyserver.go @@ -0,0 +1,63 @@ +package testserver + +import ( + "io/ioutil" + "net" + "os" + "path" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + + pb "gitlab.com/gitlab-org/gitaly-proto/go/gitalypb" +) + +type testGitalyServer struct{} + +func (s *testGitalyServer) SSHReceivePack(stream pb.SSHService_SSHReceivePackServer) error { + req, err := stream.Recv() + + if err != nil { + return err + } + + response := []byte("ReceivePack: " + req.GlId + " " + req.Repository.GlRepository) + stream.Send(&pb.SSHReceivePackResponse{Stdout: response}) + + return nil +} + +func (s *testGitalyServer) SSHUploadPack(stream pb.SSHService_SSHUploadPackServer) error { + return nil +} + +func (s *testGitalyServer) SSHUploadArchive(stream pb.SSHService_SSHUploadArchiveServer) error { + return nil +} + +func StartGitalyServer(t *testing.T) (string, func()) { + tempDir, _ := ioutil.TempDir("", "gitlab-shell-test-api") + gitalySocketPath := path.Join(tempDir, "gitaly.sock") + + err := os.MkdirAll(filepath.Dir(gitalySocketPath), 0700) + require.NoError(t, err) + + server := grpc.NewServer() + + listener, err := net.Listen("unix", gitalySocketPath) + require.NoError(t, err) + + pb.RegisterSSHServiceServer(server, &testGitalyServer{}) + + go server.Serve(listener) + + gitalySocketUrl := "unix:" + gitalySocketPath + cleanup := func() { + server.Stop() + os.RemoveAll(tempDir) + } + + return gitalySocketUrl, cleanup +} diff --git a/go/internal/gitlabnet/testserver/testserver.go b/go/internal/gitlabnet/testserver/testserver.go index bf896e6..bf59ce4 100644 --- a/go/internal/gitlabnet/testserver/testserver.go +++ b/go/internal/gitlabnet/testserver/testserver.go @@ -10,6 +10,9 @@ import ( "os" "path" "path/filepath" + "testing" + + "github.com/stretchr/testify/require" "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" ) @@ -24,15 +27,12 @@ type TestRequestHandler struct { Handler func(w http.ResponseWriter, r *http.Request) } -func StartSocketHttpServer(handlers []TestRequestHandler) (func(), string, error) { - if err := os.MkdirAll(filepath.Dir(testSocket), 0700); err != nil { - return nil, "", err - } +func StartSocketHttpServer(t *testing.T, handlers []TestRequestHandler) (string, func()) { + err := os.MkdirAll(filepath.Dir(testSocket), 0700) + require.NoError(t, err) socketListener, err := net.Listen("unix", testSocket) - if err != nil { - return nil, "", err - } + require.NoError(t, err) server := http.Server{ Handler: buildHandler(handlers), @@ -44,30 +44,27 @@ func StartSocketHttpServer(handlers []TestRequestHandler) (func(), string, error url := "http+unix://" + testSocket - return cleanupSocket, url, nil + return url, cleanupSocket } -func StartHttpServer(handlers []TestRequestHandler) (func(), string, error) { +func StartHttpServer(t *testing.T, handlers []TestRequestHandler) (string, func()) { server := httptest.NewServer(buildHandler(handlers)) - return server.Close, server.URL, nil + return server.URL, server.Close } -func StartHttpsServer(handlers []TestRequestHandler) (func(), string, error) { +func StartHttpsServer(t *testing.T, handlers []TestRequestHandler) (string, func()) { crt := path.Join(testhelper.TestRoot, "certs/valid/server.crt") key := path.Join(testhelper.TestRoot, "certs/valid/server.key") server := httptest.NewUnstartedServer(buildHandler(handlers)) cer, err := tls.LoadX509KeyPair(crt, key) - - if err != nil { - return nil, "", err - } + require.NoError(t, err) server.TLS = &tls.Config{Certificates: []tls.Certificate{cer}} server.StartTLS() - return server.Close, server.URL, nil + return server.URL, server.Close } func cleanupSocket() { diff --git a/go/internal/gitlabnet/twofactorrecover/client_test.go b/go/internal/gitlabnet/twofactorrecover/client_test.go index 56f7958..4b15ac5 100644 --- a/go/internal/gitlabnet/twofactorrecover/client_test.go +++ b/go/internal/gitlabnet/twofactorrecover/client_test.go @@ -149,8 +149,7 @@ func TestErrorResponses(t *testing.T) { func setup(t *testing.T) (*Client, func()) { initialize(t) - cleanup, url, err := testserver.StartSocketHttpServer(requests) - require.NoError(t, err) + url, cleanup := testserver.StartSocketHttpServer(t, requests) client, err := NewClient(&config.Config{GitlabUrl: url}) require.NoError(t, err) |