diff options
author | Igor Drozdov <idrozdov@gitlab.com> | 2019-04-28 20:42:19 +0300 |
---|---|---|
committer | Igor Drozdov <idrozdov@gitlab.com> | 2019-06-06 13:21:58 +0300 |
commit | 5aa947ed705b48b2980894012eb32e7ee5147b5e (patch) | |
tree | 3557c79227b01376a90f381c5f60f0ef42727baa /go | |
parent | eb2b186f7d209a638b7523c674bc79cbafe764b6 (diff) | |
download | gitlab-shell-id-git-lfs-authenticate.tar.gz |
Go implementation for LFS authenticateid-git-lfs-authenticate
Diffstat (limited to 'go')
-rw-r--r-- | go/internal/command/command.go | 3 | ||||
-rw-r--r-- | go/internal/command/command_test.go | 13 | ||||
-rw-r--r-- | go/internal/command/commandargs/command_args.go | 1 | ||||
-rw-r--r-- | go/internal/command/commandargs/command_args_test.go | 7 | ||||
-rw-r--r-- | go/internal/command/lfsauthenticate/lfsauthenticate.go | 104 | ||||
-rw-r--r-- | go/internal/command/lfsauthenticate/lfsauthenticate_test.go | 153 | ||||
-rw-r--r-- | go/internal/gitlabnet/lfsauthenticate/client.go | 66 | ||||
-rw-r--r-- | go/internal/gitlabnet/lfsauthenticate/client_test.go | 117 |
8 files changed, 464 insertions, 0 deletions
diff --git a/go/internal/command/command.go b/go/internal/command/command.go index 7bc1994..a1dde42 100644 --- a/go/internal/command/command.go +++ b/go/internal/command/command.go @@ -4,6 +4,7 @@ import ( "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs" "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/discover" "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/fallback" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/lfsauthenticate" "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/readwriter" "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/receivepack" "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/twofactorrecover" @@ -38,6 +39,8 @@ func buildCommand(args *commandargs.CommandArgs, config *config.Config, readWrit return &discover.Command{Config: config, Args: args, ReadWriter: readWriter} case commandargs.TwoFactorRecover: return &twofactorrecover.Command{Config: config, Args: args, ReadWriter: readWriter} + case commandargs.LfsAuthenticate: + return &lfsauthenticate.Command{Config: config, Args: args, ReadWriter: readWriter} case commandargs.ReceivePack: return &receivepack.Command{Config: config, Args: args, ReadWriter: readWriter} case commandargs.UploadPack: diff --git a/go/internal/command/command_test.go b/go/internal/command/command_test.go index cbdfc56..07260dd 100644 --- a/go/internal/command/command_test.go +++ b/go/internal/command/command_test.go @@ -7,6 +7,7 @@ import ( "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/discover" "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/fallback" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/lfsauthenticate" "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/receivepack" "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/twofactorrecover" "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/uploadarchive" @@ -59,6 +60,18 @@ func TestNew(t *testing.T) { expectedType: &twofactorrecover.Command{}, }, { + desc: "it returns an LfsAuthenticate command if the feature is enabled", + config: &config.Config{ + GitlabUrl: "http+unix://gitlab.socket", + Migration: config.MigrationConfig{Enabled: true, Features: []string{"git-lfs-authenticate"}}, + }, + environment: map[string]string{ + "SSH_CONNECTION": "1", + "SSH_ORIGINAL_COMMAND": "git-lfs-authenticate", + }, + expectedType: &lfsauthenticate.Command{}, + }, + { desc: "it returns a ReceivePack command if the feature is enabled", config: &config.Config{ GitlabUrl: "http+unix://gitlab.socket", diff --git a/go/internal/command/commandargs/command_args.go b/go/internal/command/commandargs/command_args.go index fd9d741..d8fe32d 100644 --- a/go/internal/command/commandargs/command_args.go +++ b/go/internal/command/commandargs/command_args.go @@ -13,6 +13,7 @@ type CommandType string const ( Discover CommandType = "discover" TwoFactorRecover CommandType = "2fa_recovery_codes" + LfsAuthenticate CommandType = "git-lfs-authenticate" ReceivePack CommandType = "git-receive-pack" UploadPack CommandType = "git-upload-pack" UploadArchive CommandType = "git-upload-archive" diff --git a/go/internal/command/commandargs/command_args_test.go b/go/internal/command/commandargs/command_args_test.go index 7c360ad..e60bb92 100644 --- a/go/internal/command/commandargs/command_args_test.go +++ b/go/internal/command/commandargs/command_args_test.go @@ -90,6 +90,13 @@ func TestParseSuccess(t *testing.T) { "SSH_ORIGINAL_COMMAND": "git-upload-archive 'group/repo'", }, expectedArgs: &CommandArgs{SshArgs: []string{"git-upload-archive", "group/repo"}, CommandType: UploadArchive}, + }, { + desc: "It parses git-lfs-authenticate command", + environment: map[string]string{ + "SSH_CONNECTION": "1", + "SSH_ORIGINAL_COMMAND": "git-lfs-authenticate 'group/repo' download", + }, + expectedArgs: &CommandArgs{SshArgs: []string{"git-lfs-authenticate", "group/repo", "download"}, CommandType: LfsAuthenticate}, }, } diff --git a/go/internal/command/lfsauthenticate/lfsauthenticate.go b/go/internal/command/lfsauthenticate/lfsauthenticate.go new file mode 100644 index 0000000..c1dc45f --- /dev/null +++ b/go/internal/command/lfsauthenticate/lfsauthenticate.go @@ -0,0 +1,104 @@ +package lfsauthenticate + +import ( + "encoding/base64" + "encoding/json" + "fmt" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/readwriter" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/shared/accessverifier" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/shared/disallowedcommand" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/lfsauthenticate" +) + +const ( + downloadAction = "download" + uploadAction = "upload" +) + +type Command struct { + Config *config.Config + Args *commandargs.CommandArgs + ReadWriter *readwriter.ReadWriter +} + +type PayloadHeader struct { + Auth string `json:"Authorization"` +} + +type Payload struct { + Header PayloadHeader `json:"header"` + Href string `json:"href"` + ExpiresIn int `json:"expires_in,omitempty"` +} + +func (c *Command) Execute() error { + args := c.Args.SshArgs + if len(args) < 3 { + return disallowedcommand.Error + } + + repo := args[1] + action, err := actionToCommandType(args[2]) + if err != nil { + return err + } + + accessResponse, err := c.verifyAccess(action, repo) + if err != nil { + return err + } + + payload, err := c.authenticate(action, repo, accessResponse.UserId) + if err != nil { + // return nothing just like Ruby's GitlabShell#lfs_authenticate does + return nil + } + + fmt.Fprintf(c.ReadWriter.Out, "%s\n", payload) + + return nil +} + +func actionToCommandType(action string) (commandargs.CommandType, error) { + var accessAction commandargs.CommandType + switch action { + case downloadAction: + accessAction = commandargs.UploadPack + case uploadAction: + accessAction = commandargs.ReceivePack + default: + return "", disallowedcommand.Error + } + + return accessAction, nil +} + +func (c *Command) verifyAccess(action commandargs.CommandType, repo string) (*accessverifier.Response, error) { + cmd := accessverifier.Command{c.Config, c.Args, c.ReadWriter} + + return cmd.Verify(action, repo) +} + +func (c *Command) authenticate(action commandargs.CommandType, repo, userId string) ([]byte, error) { + client, err := lfsauthenticate.NewClient(c.Config, c.Args) + if err != nil { + return nil, err + } + + response, err := client.Authenticate(action, repo, userId) + if err != nil { + return nil, err + } + + basicAuth := base64.StdEncoding.EncodeToString([]byte(response.Username + ":" + response.LfsToken)) + payload := &Payload{ + Header: PayloadHeader{Auth: "Basic " + basicAuth}, + Href: response.RepoPath + "/info/lfs", + ExpiresIn: response.ExpiresIn, + } + + return json.Marshal(payload) +} diff --git a/go/internal/command/lfsauthenticate/lfsauthenticate_test.go b/go/internal/command/lfsauthenticate/lfsauthenticate_test.go new file mode 100644 index 0000000..30da94b --- /dev/null +++ b/go/internal/command/lfsauthenticate/lfsauthenticate_test.go @@ -0,0 +1,153 @@ +package lfsauthenticate + +import ( + "bytes" + "encoding/json" + "io/ioutil" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/readwriter" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/accessverifier" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/lfsauthenticate" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper/requesthandlers" +) + +func TestFailedRequests(t *testing.T) { + requests := requesthandlers.BuildDisallowedByApiHandlers(t) + url, cleanup := testserver.StartHttpServer(t, requests) + defer cleanup() + + testCases := []struct { + desc string + arguments *commandargs.CommandArgs + expectedOutput string + }{ + { + desc: "With missing arguments", + arguments: &commandargs.CommandArgs{}, + expectedOutput: "> GitLab: Disallowed command", + }, + { + desc: "With disallowed command", + arguments: &commandargs.CommandArgs{GitlabKeyId: "1", SshArgs: []string{"git-lfs-authenticate", "group/repo", "unknown"}}, + expectedOutput: "> GitLab: Disallowed command", + }, + { + desc: "With disallowed user", + arguments: &commandargs.CommandArgs{GitlabKeyId: "disallowed", SshArgs: []string{"git-lfs-authenticate", "group/repo", "download"}}, + expectedOutput: "Disallowed by API call", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + output := &bytes.Buffer{} + cmd := &Command{ + Config: &config.Config{GitlabUrl: url}, + Args: tc.arguments, + ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output}, + } + + err := cmd.Execute() + require.Error(t, err) + + require.Equal(t, tc.expectedOutput, err.Error()) + }) + } +} + +func TestLfsAuthenticateRequests(t *testing.T) { + userId := "123" + + requests := []testserver.TestRequestHandler{ + { + Path: "/api/v4/internal/lfs_authenticate", + Handler: func(w http.ResponseWriter, r *http.Request) { + b, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + require.NoError(t, err) + + var request *lfsauthenticate.Request + require.NoError(t, json.Unmarshal(b, &request)) + + if request.UserId == userId { + body := map[string]interface{}{ + "username": "john", + "lfs_token": "sometoken", + "repository_http_path": "https://gitlab.com/repo/path", + "expires_in": 1800, + } + require.NoError(t, json.NewEncoder(w).Encode(body)) + } else { + w.WriteHeader(http.StatusForbidden) + } + }, + }, + { + Path: "/api/v4/internal/allowed", + Handler: func(w http.ResponseWriter, r *http.Request) { + b, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + require.NoError(t, err) + + var request *accessverifier.Request + require.NoError(t, json.Unmarshal(b, &request)) + + var glId string + if request.Username == "somename" { + glId = userId + } else { + glId = "100" + } + + body := map[string]interface{}{ + "gl_id": glId, + "status": true, + } + require.NoError(t, json.NewEncoder(w).Encode(body)) + }, + }, + } + + url, cleanup := testserver.StartHttpServer(t, requests) + defer cleanup() + + testCases := []struct { + desc string + username string + expectedOutput string + }{ + { + desc: "With successful response from API", + username: "somename", + expectedOutput: "{\"header\":{\"Authorization\":\"Basic am9objpzb21ldG9rZW4=\"},\"href\":\"https://gitlab.com/repo/path/info/lfs\",\"expires_in\":1800}\n", + }, + { + desc: "With forbidden response from API", + username: "anothername", + expectedOutput: "", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + output := &bytes.Buffer{} + cmd := &Command{ + Config: &config.Config{GitlabUrl: url}, + Args: &commandargs.CommandArgs{GitlabUsername: tc.username, SshArgs: []string{"git-lfs-authenticate", "group/repo", "upload"}}, + ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output}, + } + + err := cmd.Execute() + require.NoError(t, err) + + require.Equal(t, tc.expectedOutput, output.String()) + }) + } +} diff --git a/go/internal/gitlabnet/lfsauthenticate/client.go b/go/internal/gitlabnet/lfsauthenticate/client.go new file mode 100644 index 0000000..2a7cb03 --- /dev/null +++ b/go/internal/gitlabnet/lfsauthenticate/client.go @@ -0,0 +1,66 @@ +package lfsauthenticate + +import ( + "fmt" + "net/http" + "strings" + + "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" +) + +type Client struct { + config *config.Config + client *gitlabnet.GitlabClient + args *commandargs.CommandArgs +} + +type Request struct { + Action commandargs.CommandType `json:"operation"` + Repo string `json:"project"` + KeyId string `json:"key_id,omitempty"` + UserId string `json:"user_id,omitempty"` +} + +type Response struct { + Username string `json:"username"` + LfsToken string `json:"lfs_token"` + RepoPath string `json:"repository_http_path"` + ExpiresIn int `json:"expires_in"` +} + +func NewClient(config *config.Config, args *commandargs.CommandArgs) (*Client, error) { + client, err := gitlabnet.GetClient(config) + if err != nil { + return nil, fmt.Errorf("Error creating http client: %v", err) + } + + return &Client{config: config, client: client, args: args}, nil +} + +func (c *Client) Authenticate(action commandargs.CommandType, repo, userId string) (*Response, error) { + request := &Request{Action: action, Repo: repo} + if c.args.GitlabKeyId != "" { + request.KeyId = c.args.GitlabKeyId + } else { + request.UserId = strings.TrimPrefix(userId, "user-") + } + + response, err := c.client.Post("/lfs_authenticate", request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + return parse(response) +} + +func parse(hr *http.Response) (*Response, error) { + response := &Response{} + if err := gitlabnet.ParseJSON(hr, response); err != nil { + return nil, err + } + + return response, nil +} diff --git a/go/internal/gitlabnet/lfsauthenticate/client_test.go b/go/internal/gitlabnet/lfsauthenticate/client_test.go new file mode 100644 index 0000000..7fd7aca --- /dev/null +++ b/go/internal/gitlabnet/lfsauthenticate/client_test.go @@ -0,0 +1,117 @@ +package lfsauthenticate + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "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/testserver" +) + +const ( + keyId = "123" + repo = "group/repo" + action = commandargs.UploadPack +) + +func setup(t *testing.T) []testserver.TestRequestHandler { + requests := []testserver.TestRequestHandler{ + { + Path: "/api/v4/internal/lfs_authenticate", + Handler: func(w http.ResponseWriter, r *http.Request) { + b, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + require.NoError(t, err) + + var request *Request + require.NoError(t, json.Unmarshal(b, &request)) + + switch request.KeyId { + case keyId: + body := map[string]interface{}{ + "username": "john", + "lfs_token": "sometoken", + "repository_http_path": "https://gitlab.com/repo/path", + "expires_in": 1800, + } + require.NoError(t, json.NewEncoder(w).Encode(body)) + case "forbidden": + w.WriteHeader(http.StatusForbidden) + case "broken": + w.WriteHeader(http.StatusInternalServerError) + } + }, + }, + } + + return requests +} + +func TestFailedRequests(t *testing.T) { + requests := setup(t) + url, cleanup := testserver.StartHttpServer(t, requests) + defer cleanup() + + testCases := []struct { + desc string + args *commandargs.CommandArgs + expectedOutput string + }{ + { + desc: "With bad response", + args: &commandargs.CommandArgs{GitlabKeyId: "-1", CommandType: commandargs.UploadPack}, + expectedOutput: "Parsing failed", + }, + { + desc: "With API returns an error", + args: &commandargs.CommandArgs{GitlabKeyId: "forbidden", CommandType: commandargs.UploadPack}, + expectedOutput: "Internal API error (403)", + }, + { + desc: "With API fails", + args: &commandargs.CommandArgs{GitlabKeyId: "broken", CommandType: commandargs.UploadPack}, + expectedOutput: "Internal API error (500)", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + client, err := NewClient(&config.Config{GitlabUrl: url}, tc.args) + require.NoError(t, err) + + repo := "group/repo" + + _, err = client.Authenticate(tc.args.CommandType, repo, "") + require.Error(t, err) + + require.Equal(t, tc.expectedOutput, err.Error()) + }) + } +} + +func TestSuccessfulRequests(t *testing.T) { + requests := setup(t) + url, cleanup := testserver.StartHttpServer(t, requests) + defer cleanup() + + args := &commandargs.CommandArgs{GitlabKeyId: keyId, CommandType: commandargs.LfsAuthenticate} + client, err := NewClient(&config.Config{GitlabUrl: url}, args) + require.NoError(t, err) + + response, err := client.Authenticate(action, repo, "") + require.NoError(t, err) + + expectedResponse := &Response{ + Username: "john", + LfsToken: "sometoken", + RepoPath: "https://gitlab.com/repo/path", + ExpiresIn: 1800, + } + + require.Equal(t, expectedResponse, response) +} |