summaryrefslogtreecommitdiff
path: root/internal/command
diff options
context:
space:
mode:
authorNick Thomas <nick@gitlab.com>2019-10-17 12:04:52 +0100
committerNick Thomas <nick@gitlab.com>2019-10-18 11:47:25 +0100
commit83d11f4deeb20b852a0af3433190a0f7250a0027 (patch)
tree1a9df18d6f9f59712c6f5c98e995a4918eb94a11 /internal/command
parent7d5229db263a62661653431881bef8b46984d0de (diff)
downloadgitlab-shell-83d11f4deeb20b852a0af3433190a0f7250a0027.tar.gz
Move go code up one level
Diffstat (limited to 'internal/command')
-rw-r--r--internal/command/authorizedkeys/authorized_keys.go61
-rw-r--r--internal/command/authorizedkeys/authorized_keys_test.go90
-rw-r--r--internal/command/authorizedprincipals/authorized_principals.go47
-rw-r--r--internal/command/authorizedprincipals/authorized_principals_test.go47
-rw-r--r--internal/command/command.go81
-rw-r--r--internal/command/command_test.go146
-rw-r--r--internal/command/commandargs/authorized_keys.go51
-rw-r--r--internal/command/commandargs/authorized_principals.go50
-rw-r--r--internal/command/commandargs/command_args.go31
-rw-r--r--internal/command/commandargs/command_args_test.go231
-rw-r--r--internal/command/commandargs/generic_args.go14
-rw-r--r--internal/command/commandargs/shell.go131
-rw-r--r--internal/command/discover/discover.go40
-rw-r--r--internal/command/discover/discover_test.go135
-rw-r--r--internal/command/healthcheck/healthcheck.go49
-rw-r--r--internal/command/healthcheck/healthcheck_test.go90
-rw-r--r--internal/command/lfsauthenticate/lfsauthenticate.go104
-rw-r--r--internal/command/lfsauthenticate/lfsauthenticate_test.go153
-rw-r--r--internal/command/readwriter/readwriter.go9
-rw-r--r--internal/command/receivepack/customaction.go99
-rw-r--r--internal/command/receivepack/customaction_test.go105
-rw-r--r--internal/command/receivepack/gitalycall.go39
-rw-r--r--internal/command/receivepack/gitalycall_test.go40
-rw-r--r--internal/command/receivepack/receivepack.go40
-rw-r--r--internal/command/receivepack/receivepack_test.go32
-rw-r--r--internal/command/shared/accessverifier/accessverifier.go45
-rw-r--r--internal/command/shared/accessverifier/accessverifier_test.go82
-rw-r--r--internal/command/shared/disallowedcommand/disallowedcommand.go7
-rw-r--r--internal/command/twofactorrecover/twofactorrecover.go65
-rw-r--r--internal/command/twofactorrecover/twofactorrecover_test.go136
-rw-r--r--internal/command/uploadarchive/gitalycall.go32
-rw-r--r--internal/command/uploadarchive/gitalycall_test.go40
-rw-r--r--internal/command/uploadarchive/uploadarchive.go36
-rw-r--r--internal/command/uploadarchive/uploadarchive_test.go31
-rw-r--r--internal/command/uploadpack/gitalycall.go36
-rw-r--r--internal/command/uploadpack/gitalycall_test.go40
-rw-r--r--internal/command/uploadpack/uploadpack.go36
-rw-r--r--internal/command/uploadpack/uploadpack_test.go31
38 files changed, 2532 insertions, 0 deletions
diff --git a/internal/command/authorizedkeys/authorized_keys.go b/internal/command/authorizedkeys/authorized_keys.go
new file mode 100644
index 0000000..d5837b0
--- /dev/null
+++ b/internal/command/authorizedkeys/authorized_keys.go
@@ -0,0 +1,61 @@
+package authorizedkeys
+
+import (
+ "fmt"
+ "strconv"
+
+ "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/authorizedkeys"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/keyline"
+)
+
+type Command struct {
+ Config *config.Config
+ Args *commandargs.AuthorizedKeys
+ ReadWriter *readwriter.ReadWriter
+}
+
+func (c *Command) Execute() error {
+ // Do and return nothing when the expected and actual user don't match.
+ // This can happen when the user in sshd_config doesn't match the user
+ // trying to login. When nothing is printed, the user will be denied access.
+ if c.Args.ExpectedUser != c.Args.ActualUser {
+ // TODO: Log this event once we have a consistent way to log in Go.
+ // See https://gitlab.com/gitlab-org/gitlab-shell/issues/192 for more info.
+ return nil
+ }
+
+ if err := c.printKeyLine(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *Command) printKeyLine() error {
+ response, err := c.getAuthorizedKey()
+ if err != nil {
+ fmt.Fprintln(c.ReadWriter.Out, fmt.Sprintf("# No key was found for %s", c.Args.Key))
+ return nil
+ }
+
+ keyLine, err := keyline.NewPublicKeyLine(strconv.FormatInt(response.Id, 10), response.Key, c.Config.RootDir)
+ if err != nil {
+ return err
+ }
+
+ fmt.Fprintln(c.ReadWriter.Out, keyLine.ToString())
+
+ return nil
+}
+
+func (c *Command) getAuthorizedKey() (*authorizedkeys.Response, error) {
+ client, err := authorizedkeys.NewClient(c.Config)
+ if err != nil {
+ return nil, err
+ }
+
+ return client.GetByKey(c.Args.Key)
+}
diff --git a/internal/command/authorizedkeys/authorized_keys_test.go b/internal/command/authorizedkeys/authorized_keys_test.go
new file mode 100644
index 0000000..5cde366
--- /dev/null
+++ b/internal/command/authorizedkeys/authorized_keys_test.go
@@ -0,0 +1,90 @@
+package authorizedkeys
+
+import (
+ "bytes"
+ "encoding/json"
+ "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/testserver"
+)
+
+var (
+ requests = []testserver.TestRequestHandler{
+ {
+ Path: "/api/v4/internal/authorized_keys",
+ Handler: func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Query().Get("key") == "key" {
+ body := map[string]interface{}{
+ "id": 1,
+ "key": "public-key",
+ }
+ json.NewEncoder(w).Encode(body)
+ } else if r.URL.Query().Get("key") == "broken-message" {
+ body := map[string]string{
+ "message": "Forbidden!",
+ }
+ w.WriteHeader(http.StatusForbidden)
+ json.NewEncoder(w).Encode(body)
+ } else if r.URL.Query().Get("key") == "broken" {
+ w.WriteHeader(http.StatusInternalServerError)
+ } else {
+ w.WriteHeader(http.StatusNotFound)
+ }
+ },
+ },
+ }
+)
+
+func TestExecute(t *testing.T) {
+ url, cleanup := testserver.StartSocketHttpServer(t, requests)
+ defer cleanup()
+
+ testCases := []struct {
+ desc string
+ arguments *commandargs.AuthorizedKeys
+ expectedOutput string
+ }{
+ {
+ desc: "With matching username and key",
+ arguments: &commandargs.AuthorizedKeys{ExpectedUser: "user", ActualUser: "user", Key: "key"},
+ expectedOutput: "command=\"/tmp/bin/gitlab-shell key-1\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty public-key\n",
+ },
+ {
+ desc: "When key doesn't match any existing key",
+ arguments: &commandargs.AuthorizedKeys{ExpectedUser: "user", ActualUser: "user", Key: "not-found"},
+ expectedOutput: "# No key was found for not-found\n",
+ },
+ {
+ desc: "When the API returns an error",
+ arguments: &commandargs.AuthorizedKeys{ExpectedUser: "user", ActualUser: "user", Key: "broken-message"},
+ expectedOutput: "# No key was found for broken-message\n",
+ },
+ {
+ desc: "When the API fails",
+ arguments: &commandargs.AuthorizedKeys{ExpectedUser: "user", ActualUser: "user", Key: "broken"},
+ expectedOutput: "# No key was found for broken\n",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ buffer := &bytes.Buffer{}
+ cmd := &Command{
+ Config: &config.Config{RootDir: "/tmp", GitlabUrl: url},
+ Args: tc.arguments,
+ ReadWriter: &readwriter.ReadWriter{Out: buffer},
+ }
+
+ err := cmd.Execute()
+
+ require.NoError(t, err)
+ require.Equal(t, tc.expectedOutput, buffer.String())
+ })
+ }
+}
diff --git a/internal/command/authorizedprincipals/authorized_principals.go b/internal/command/authorizedprincipals/authorized_principals.go
new file mode 100644
index 0000000..b04e5a4
--- /dev/null
+++ b/internal/command/authorizedprincipals/authorized_principals.go
@@ -0,0 +1,47 @@
+package authorizedprincipals
+
+import (
+ "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/config"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/keyline"
+)
+
+type Command struct {
+ Config *config.Config
+ Args *commandargs.AuthorizedPrincipals
+ ReadWriter *readwriter.ReadWriter
+}
+
+func (c *Command) Execute() error {
+ if err := c.printPrincipalLines(); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (c *Command) printPrincipalLines() error {
+ principals := c.Args.Principals
+
+ for _, principal := range principals {
+ if err := c.printPrincipalLine(principal); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (c *Command) printPrincipalLine(principal string) error {
+ principalKeyLine, err := keyline.NewPrincipalKeyLine(c.Args.KeyId, principal, c.Config.RootDir)
+ if err != nil {
+ return err
+ }
+
+ fmt.Fprintln(c.ReadWriter.Out, principalKeyLine.ToString())
+
+ return nil
+}
diff --git a/internal/command/authorizedprincipals/authorized_principals_test.go b/internal/command/authorizedprincipals/authorized_principals_test.go
new file mode 100644
index 0000000..2db0d41
--- /dev/null
+++ b/internal/command/authorizedprincipals/authorized_principals_test.go
@@ -0,0 +1,47 @@
+package authorizedprincipals
+
+import (
+ "bytes"
+ "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"
+)
+
+func TestExecute(t *testing.T) {
+ testCases := []struct {
+ desc string
+ arguments *commandargs.AuthorizedPrincipals
+ expectedOutput string
+ }{
+ {
+ desc: "With single principal",
+ arguments: &commandargs.AuthorizedPrincipals{KeyId: "key", Principals: []string{"principal"}},
+ expectedOutput: "command=\"/tmp/bin/gitlab-shell username-key\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty principal\n",
+ },
+ {
+ desc: "With multiple principals",
+ arguments: &commandargs.AuthorizedPrincipals{KeyId: "key", Principals: []string{"principal-1", "principal-2"}},
+ expectedOutput: "command=\"/tmp/bin/gitlab-shell username-key\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty principal-1\ncommand=\"/tmp/bin/gitlab-shell username-key\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty principal-2\n",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ buffer := &bytes.Buffer{}
+ cmd := &Command{
+ Config: &config.Config{RootDir: "/tmp"},
+ Args: tc.arguments,
+ ReadWriter: &readwriter.ReadWriter{Out: buffer},
+ }
+
+ err := cmd.Execute()
+
+ require.NoError(t, err)
+ require.Equal(t, tc.expectedOutput, buffer.String())
+ })
+ }
+}
diff --git a/internal/command/command.go b/internal/command/command.go
new file mode 100644
index 0000000..52393df
--- /dev/null
+++ b/internal/command/command.go
@@ -0,0 +1,81 @@
+package command
+
+import (
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/authorizedkeys"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/authorizedprincipals"
+ "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/healthcheck"
+ "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/shared/disallowedcommand"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/twofactorrecover"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/uploadarchive"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/uploadpack"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/config"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/executable"
+)
+
+type Command interface {
+ Execute() error
+}
+
+func New(e *executable.Executable, arguments []string, config *config.Config, readWriter *readwriter.ReadWriter) (Command, error) {
+ args, err := commandargs.Parse(e, arguments)
+ if err != nil {
+ return nil, err
+ }
+
+ if cmd := buildCommand(e, args, config, readWriter); cmd != nil {
+ return cmd, nil
+ }
+
+ return nil, disallowedcommand.Error
+}
+
+func buildCommand(e *executable.Executable, args commandargs.CommandArgs, config *config.Config, readWriter *readwriter.ReadWriter) Command {
+ switch e.Name {
+ case executable.GitlabShell:
+ return buildShellCommand(args.(*commandargs.Shell), config, readWriter)
+ case executable.AuthorizedKeysCheck:
+ return buildAuthorizedKeysCommand(args.(*commandargs.AuthorizedKeys), config, readWriter)
+ case executable.AuthorizedPrincipalsCheck:
+ return buildAuthorizedPrincipalsCommand(args.(*commandargs.AuthorizedPrincipals), config, readWriter)
+ case executable.Healthcheck:
+ return buildHealthcheckCommand(config, readWriter)
+ }
+
+ return nil
+}
+
+func buildShellCommand(args *commandargs.Shell, config *config.Config, readWriter *readwriter.ReadWriter) Command {
+ switch args.CommandType {
+ case commandargs.Discover:
+ 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:
+ return &uploadpack.Command{Config: config, Args: args, ReadWriter: readWriter}
+ case commandargs.UploadArchive:
+ return &uploadarchive.Command{Config: config, Args: args, ReadWriter: readWriter}
+ }
+
+ return nil
+}
+
+func buildAuthorizedKeysCommand(args *commandargs.AuthorizedKeys, config *config.Config, readWriter *readwriter.ReadWriter) Command {
+ return &authorizedkeys.Command{Config: config, Args: args, ReadWriter: readWriter}
+}
+
+func buildAuthorizedPrincipalsCommand(args *commandargs.AuthorizedPrincipals, config *config.Config, readWriter *readwriter.ReadWriter) Command {
+ return &authorizedprincipals.Command{Config: config, Args: args, ReadWriter: readWriter}
+}
+
+func buildHealthcheckCommand(config *config.Config, readWriter *readwriter.ReadWriter) Command {
+ return &healthcheck.Command{Config: config, ReadWriter: readWriter}
+}
diff --git a/internal/command/command_test.go b/internal/command/command_test.go
new file mode 100644
index 0000000..cd3ac9b
--- /dev/null
+++ b/internal/command/command_test.go
@@ -0,0 +1,146 @@
+package command
+
+import (
+ "errors"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/authorizedkeys"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/authorizedprincipals"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/discover"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/healthcheck"
+ "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/shared/disallowedcommand"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/twofactorrecover"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/uploadarchive"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/uploadpack"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/config"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/executable"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper"
+)
+
+var (
+ authorizedKeysExec = &executable.Executable{Name: executable.AuthorizedKeysCheck}
+ authorizedPrincipalsExec = &executable.Executable{Name: executable.AuthorizedPrincipalsCheck}
+ checkExec = &executable.Executable{Name: executable.Healthcheck}
+ gitlabShellExec = &executable.Executable{Name: executable.GitlabShell}
+
+ basicConfig = &config.Config{GitlabUrl: "http+unix://gitlab.socket"}
+)
+
+func buildEnv(command string) map[string]string {
+ return map[string]string{
+ "SSH_CONNECTION": "1",
+ "SSH_ORIGINAL_COMMAND": command,
+ }
+}
+
+func TestNew(t *testing.T) {
+ testCases := []struct {
+ desc string
+ executable *executable.Executable
+ environment map[string]string
+ arguments []string
+ expectedType interface{}
+ }{
+ {
+ desc: "it returns a Discover command",
+ executable: gitlabShellExec,
+ environment: buildEnv(""),
+ expectedType: &discover.Command{},
+ },
+ {
+ desc: "it returns a TwoFactorRecover command",
+ executable: gitlabShellExec,
+ environment: buildEnv("2fa_recovery_codes"),
+ expectedType: &twofactorrecover.Command{},
+ },
+ {
+ desc: "it returns an LfsAuthenticate command",
+ executable: gitlabShellExec,
+ environment: buildEnv("git-lfs-authenticate"),
+ expectedType: &lfsauthenticate.Command{},
+ },
+ {
+ desc: "it returns a ReceivePack command",
+ executable: gitlabShellExec,
+ environment: buildEnv("git-receive-pack"),
+ expectedType: &receivepack.Command{},
+ },
+ {
+ desc: "it returns an UploadPack command",
+ executable: gitlabShellExec,
+ environment: buildEnv("git-upload-pack"),
+ expectedType: &uploadpack.Command{},
+ },
+ {
+ desc: "it returns an UploadArchive command",
+ executable: gitlabShellExec,
+ environment: buildEnv("git-upload-archive"),
+ expectedType: &uploadarchive.Command{},
+ },
+ {
+ desc: "it returns a Healthcheck command",
+ executable: checkExec,
+ expectedType: &healthcheck.Command{},
+ },
+ {
+ desc: "it returns a AuthorizedKeys command",
+ executable: authorizedKeysExec,
+ arguments: []string{"git", "git", "key"},
+ expectedType: &authorizedkeys.Command{},
+ },
+ {
+ desc: "it returns a AuthorizedPrincipals command",
+ executable: authorizedPrincipalsExec,
+ arguments: []string{"key", "principal"},
+ expectedType: &authorizedprincipals.Command{},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ restoreEnv := testhelper.TempEnv(tc.environment)
+ defer restoreEnv()
+
+ command, err := New(tc.executable, tc.arguments, basicConfig, nil)
+
+ require.NoError(t, err)
+ require.IsType(t, tc.expectedType, command)
+ })
+ }
+}
+
+func TestFailingNew(t *testing.T) {
+ testCases := []struct {
+ desc string
+ executable *executable.Executable
+ environment map[string]string
+ expectedError error
+ }{
+ {
+ desc: "Parsing environment failed",
+ executable: gitlabShellExec,
+ expectedError: errors.New("Only SSH allowed"),
+ },
+ {
+ desc: "Unknown command given",
+ executable: gitlabShellExec,
+ environment: buildEnv("unknown"),
+ expectedError: disallowedcommand.Error,
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ restoreEnv := testhelper.TempEnv(tc.environment)
+ defer restoreEnv()
+
+ command, err := New(tc.executable, []string{}, basicConfig, nil)
+ require.Nil(t, command)
+ require.Equal(t, tc.expectedError, err)
+ })
+ }
+}
diff --git a/internal/command/commandargs/authorized_keys.go b/internal/command/commandargs/authorized_keys.go
new file mode 100644
index 0000000..2733954
--- /dev/null
+++ b/internal/command/commandargs/authorized_keys.go
@@ -0,0 +1,51 @@
+package commandargs
+
+import (
+ "errors"
+ "fmt"
+)
+
+type AuthorizedKeys struct {
+ Arguments []string
+ ExpectedUser string
+ ActualUser string
+ Key string
+}
+
+func (ak *AuthorizedKeys) Parse() error {
+ if err := ak.validate(); err != nil {
+ return err
+ }
+
+ ak.ExpectedUser = ak.Arguments[0]
+ ak.ActualUser = ak.Arguments[1]
+ ak.Key = ak.Arguments[2]
+
+ return nil
+}
+
+func (ak *AuthorizedKeys) GetArguments() []string {
+ return ak.Arguments
+}
+
+func (ak *AuthorizedKeys) validate() error {
+ argsSize := len(ak.Arguments)
+
+ if argsSize != 3 {
+ return errors.New(fmt.Sprintf("# Insufficient arguments. %d. Usage\n#\tgitlab-shell-authorized-keys-check <expected-username> <actual-username> <key>", argsSize))
+ }
+
+ expectedUsername := ak.Arguments[0]
+ actualUsername := ak.Arguments[1]
+ key := ak.Arguments[2]
+
+ if expectedUsername == "" || actualUsername == "" {
+ return errors.New("# No username provided")
+ }
+
+ if key == "" {
+ return errors.New("# No key provided")
+ }
+
+ return nil
+}
diff --git a/internal/command/commandargs/authorized_principals.go b/internal/command/commandargs/authorized_principals.go
new file mode 100644
index 0000000..746ae3f
--- /dev/null
+++ b/internal/command/commandargs/authorized_principals.go
@@ -0,0 +1,50 @@
+package commandargs
+
+import (
+ "errors"
+ "fmt"
+)
+
+type AuthorizedPrincipals struct {
+ Arguments []string
+ KeyId string
+ Principals []string
+}
+
+func (ap *AuthorizedPrincipals) Parse() error {
+ if err := ap.validate(); err != nil {
+ return err
+ }
+
+ ap.KeyId = ap.Arguments[0]
+ ap.Principals = ap.Arguments[1:]
+
+ return nil
+}
+
+func (ap *AuthorizedPrincipals) GetArguments() []string {
+ return ap.Arguments
+}
+
+func (ap *AuthorizedPrincipals) validate() error {
+ argsSize := len(ap.Arguments)
+
+ if argsSize < 2 {
+ return errors.New(fmt.Sprintf("# Insufficient arguments. %d. Usage\n#\tgitlab-shell-authorized-principals-check <key-id> <principal1> [<principal2>...]", argsSize))
+ }
+
+ keyId := ap.Arguments[0]
+ principals := ap.Arguments[1:]
+
+ if keyId == "" {
+ return errors.New("# No key_id provided")
+ }
+
+ for _, principal := range principals {
+ if principal == "" {
+ return errors.New("# An invalid principal was provided")
+ }
+ }
+
+ return nil
+}
diff --git a/internal/command/commandargs/command_args.go b/internal/command/commandargs/command_args.go
new file mode 100644
index 0000000..4831134
--- /dev/null
+++ b/internal/command/commandargs/command_args.go
@@ -0,0 +1,31 @@
+package commandargs
+
+import (
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/executable"
+)
+
+type CommandType string
+
+type CommandArgs interface {
+ Parse() error
+ GetArguments() []string
+}
+
+func Parse(e *executable.Executable, arguments []string) (CommandArgs, error) {
+ var args CommandArgs = &GenericArgs{Arguments: arguments}
+
+ switch e.Name {
+ case executable.GitlabShell:
+ args = &Shell{Arguments: arguments}
+ case executable.AuthorizedKeysCheck:
+ args = &AuthorizedKeys{Arguments: arguments}
+ case executable.AuthorizedPrincipalsCheck:
+ args = &AuthorizedPrincipals{Arguments: arguments}
+ }
+
+ if err := args.Parse(); err != nil {
+ return nil, err
+ }
+
+ return args, nil
+}
diff --git a/internal/command/commandargs/command_args_test.go b/internal/command/commandargs/command_args_test.go
new file mode 100644
index 0000000..9f1575d
--- /dev/null
+++ b/internal/command/commandargs/command_args_test.go
@@ -0,0 +1,231 @@
+package commandargs
+
+import (
+ "testing"
+
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/executable"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper"
+
+ "github.com/stretchr/testify/require"
+)
+
+func TestParseSuccess(t *testing.T) {
+ testCases := []struct {
+ desc string
+ executable *executable.Executable
+ environment map[string]string
+ arguments []string
+ expectedArgs CommandArgs
+ }{
+ // Setting the used env variables for every case to ensure we're
+ // not using anything set in the original env.
+ {
+ desc: "It sets discover as the command when the command string was empty",
+ executable: &executable.Executable{Name: executable.GitlabShell},
+ environment: map[string]string{
+ "SSH_CONNECTION": "1",
+ "SSH_ORIGINAL_COMMAND": "",
+ },
+ arguments: []string{},
+ expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{}, CommandType: Discover},
+ },
+ {
+ desc: "It finds the key id in any passed arguments",
+ executable: &executable.Executable{Name: executable.GitlabShell},
+ environment: map[string]string{
+ "SSH_CONNECTION": "1",
+ "SSH_ORIGINAL_COMMAND": "",
+ },
+ arguments: []string{"hello", "key-123"},
+ expectedArgs: &Shell{Arguments: []string{"hello", "key-123"}, SshArgs: []string{}, CommandType: Discover, GitlabKeyId: "123"},
+ }, {
+ desc: "It finds the username in any passed arguments",
+ executable: &executable.Executable{Name: executable.GitlabShell},
+ environment: map[string]string{
+ "SSH_CONNECTION": "1",
+ "SSH_ORIGINAL_COMMAND": "",
+ },
+ arguments: []string{"hello", "username-jane-doe"},
+ expectedArgs: &Shell{Arguments: []string{"hello", "username-jane-doe"}, SshArgs: []string{}, CommandType: Discover, GitlabUsername: "jane-doe"},
+ }, {
+ desc: "It parses 2fa_recovery_codes command",
+ executable: &executable.Executable{Name: executable.GitlabShell},
+ environment: map[string]string{
+ "SSH_CONNECTION": "1",
+ "SSH_ORIGINAL_COMMAND": "2fa_recovery_codes",
+ },
+ arguments: []string{},
+ expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"2fa_recovery_codes"}, CommandType: TwoFactorRecover},
+ }, {
+ desc: "It parses git-receive-pack command",
+ executable: &executable.Executable{Name: executable.GitlabShell},
+ environment: map[string]string{
+ "SSH_CONNECTION": "1",
+ "SSH_ORIGINAL_COMMAND": "git-receive-pack group/repo",
+ },
+ arguments: []string{},
+ expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack},
+ }, {
+ desc: "It parses git-receive-pack command and a project with single quotes",
+ executable: &executable.Executable{Name: executable.GitlabShell},
+ environment: map[string]string{
+ "SSH_CONNECTION": "1",
+ "SSH_ORIGINAL_COMMAND": "git receive-pack 'group/repo'",
+ },
+ arguments: []string{},
+ expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack},
+ }, {
+ desc: `It parses "git receive-pack" command`,
+ executable: &executable.Executable{Name: executable.GitlabShell},
+ environment: map[string]string{
+ "SSH_CONNECTION": "1",
+ "SSH_ORIGINAL_COMMAND": `git receive-pack "group/repo"`,
+ },
+ arguments: []string{},
+ expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack},
+ }, {
+ desc: `It parses a command followed by control characters`,
+ executable: &executable.Executable{Name: executable.GitlabShell},
+ environment: map[string]string{
+ "SSH_CONNECTION": "1",
+ "SSH_ORIGINAL_COMMAND": `git-receive-pack group/repo; any command`,
+ },
+ arguments: []string{},
+ expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-receive-pack", "group/repo"}, CommandType: ReceivePack},
+ }, {
+ desc: "It parses git-upload-pack command",
+ executable: &executable.Executable{Name: executable.GitlabShell},
+ environment: map[string]string{
+ "SSH_CONNECTION": "1",
+ "SSH_ORIGINAL_COMMAND": `git upload-pack "group/repo"`,
+ },
+ arguments: []string{},
+ expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-upload-pack", "group/repo"}, CommandType: UploadPack},
+ }, {
+ desc: "It parses git-upload-archive command",
+ executable: &executable.Executable{Name: executable.GitlabShell},
+ environment: map[string]string{
+ "SSH_CONNECTION": "1",
+ "SSH_ORIGINAL_COMMAND": "git-upload-archive 'group/repo'",
+ },
+ arguments: []string{},
+ expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-upload-archive", "group/repo"}, CommandType: UploadArchive},
+ }, {
+ desc: "It parses git-lfs-authenticate command",
+ executable: &executable.Executable{Name: executable.GitlabShell},
+ environment: map[string]string{
+ "SSH_CONNECTION": "1",
+ "SSH_ORIGINAL_COMMAND": "git-lfs-authenticate 'group/repo' download",
+ },
+ arguments: []string{},
+ expectedArgs: &Shell{Arguments: []string{}, SshArgs: []string{"git-lfs-authenticate", "group/repo", "download"}, CommandType: LfsAuthenticate},
+ }, {
+ desc: "It parses authorized-keys command",
+ executable: &executable.Executable{Name: executable.AuthorizedKeysCheck},
+ arguments: []string{"git", "git", "key"},
+ expectedArgs: &AuthorizedKeys{Arguments: []string{"git", "git", "key"}, ExpectedUser: "git", ActualUser: "git", Key: "key"},
+ }, {
+ desc: "It parses authorized-principals command",
+ executable: &executable.Executable{Name: executable.AuthorizedPrincipalsCheck},
+ arguments: []string{"key", "principal-1", "principal-2"},
+ expectedArgs: &AuthorizedPrincipals{Arguments: []string{"key", "principal-1", "principal-2"}, KeyId: "key", Principals: []string{"principal-1", "principal-2"}},
+ }, {
+ desc: "Unknown executable",
+ executable: &executable.Executable{Name: "unknown"},
+ arguments: []string{},
+ expectedArgs: &GenericArgs{Arguments: []string{}},
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ restoreEnv := testhelper.TempEnv(tc.environment)
+ defer restoreEnv()
+
+ result, err := Parse(tc.executable, tc.arguments)
+
+ require.NoError(t, err)
+ require.Equal(t, tc.expectedArgs, result)
+ })
+ }
+}
+
+func TestParseFailure(t *testing.T) {
+ testCases := []struct {
+ desc string
+ executable *executable.Executable
+ environment map[string]string
+ arguments []string
+ expectedError string
+ }{
+ {
+ desc: "It fails if SSH connection is not set",
+ executable: &executable.Executable{Name: executable.GitlabShell},
+ arguments: []string{},
+ expectedError: "Only SSH allowed",
+ },
+ {
+ desc: "It fails if SSH command is invalid",
+ executable: &executable.Executable{Name: executable.GitlabShell},
+ environment: map[string]string{
+ "SSH_CONNECTION": "1",
+ "SSH_ORIGINAL_COMMAND": `git receive-pack "`,
+ },
+ arguments: []string{},
+ expectedError: "Invalid SSH command",
+ },
+ {
+ desc: "With not enough arguments for the AuthorizedKeysCheck",
+ executable: &executable.Executable{Name: executable.AuthorizedKeysCheck},
+ arguments: []string{"user"},
+ expectedError: "# Insufficient arguments. 1. Usage\n#\tgitlab-shell-authorized-keys-check <expected-username> <actual-username> <key>",
+ },
+ {
+ desc: "With too many arguments for the AuthorizedKeysCheck",
+ executable: &executable.Executable{Name: executable.AuthorizedKeysCheck},
+ arguments: []string{"user", "user", "key", "something-else"},
+ expectedError: "# Insufficient arguments. 4. Usage\n#\tgitlab-shell-authorized-keys-check <expected-username> <actual-username> <key>",
+ },
+ {
+ desc: "With missing username for the AuthorizedKeysCheck",
+ executable: &executable.Executable{Name: executable.AuthorizedKeysCheck},
+ arguments: []string{"user", "", "key"},
+ expectedError: "# No username provided",
+ },
+ {
+ desc: "With missing key for the AuthorizedKeysCheck",
+ executable: &executable.Executable{Name: executable.AuthorizedKeysCheck},
+ arguments: []string{"user", "user", ""},
+ expectedError: "# No key provided",
+ },
+ {
+ desc: "With not enough arguments for the AuthorizedPrincipalsCheck",
+ executable: &executable.Executable{Name: executable.AuthorizedPrincipalsCheck},
+ arguments: []string{"key"},
+ expectedError: "# Insufficient arguments. 1. Usage\n#\tgitlab-shell-authorized-principals-check <key-id> <principal1> [<principal2>...]",
+ },
+ {
+ desc: "With missing key_id for the AuthorizedPrincipalsCheck",
+ executable: &executable.Executable{Name: executable.AuthorizedPrincipalsCheck},
+ arguments: []string{"", "principal"},
+ expectedError: "# No key_id provided",
+ },
+ {
+ desc: "With blank principal for the AuthorizedPrincipalsCheck",
+ executable: &executable.Executable{Name: executable.AuthorizedPrincipalsCheck},
+ arguments: []string{"key", "principal", ""},
+ expectedError: "# An invalid principal was provided",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ restoreEnv := testhelper.TempEnv(tc.environment)
+ defer restoreEnv()
+
+ _, err := Parse(tc.executable, tc.arguments)
+
+ require.EqualError(t, err, tc.expectedError)
+ })
+ }
+}
diff --git a/internal/command/commandargs/generic_args.go b/internal/command/commandargs/generic_args.go
new file mode 100644
index 0000000..96bed99
--- /dev/null
+++ b/internal/command/commandargs/generic_args.go
@@ -0,0 +1,14 @@
+package commandargs
+
+type GenericArgs struct {
+ Arguments []string
+}
+
+func (b *GenericArgs) Parse() error {
+ // Do nothing
+ return nil
+}
+
+func (b *GenericArgs) GetArguments() []string {
+ return b.Arguments
+}
diff --git a/internal/command/commandargs/shell.go b/internal/command/commandargs/shell.go
new file mode 100644
index 0000000..7e2b72e
--- /dev/null
+++ b/internal/command/commandargs/shell.go
@@ -0,0 +1,131 @@
+package commandargs
+
+import (
+ "errors"
+ "os"
+ "regexp"
+
+ "github.com/mattn/go-shellwords"
+)
+
+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"
+)
+
+var (
+ whoKeyRegex = regexp.MustCompile(`\bkey-(?P<keyid>\d+)\b`)
+ whoUsernameRegex = regexp.MustCompile(`\busername-(?P<username>\S+)\b`)
+)
+
+type Shell struct {
+ Arguments []string
+ GitlabUsername string
+ GitlabKeyId string
+ SshArgs []string
+ CommandType CommandType
+}
+
+func (s *Shell) Parse() error {
+ if err := s.validate(); err != nil {
+ return err
+ }
+
+ s.parseWho()
+ s.defineCommandType()
+
+ return nil
+}
+
+func (s *Shell) GetArguments() []string {
+ return s.Arguments
+}
+
+func (s *Shell) validate() error {
+ if !s.isSshConnection() {
+ return errors.New("Only SSH allowed")
+ }
+
+ if !s.isValidSshCommand() {
+ return errors.New("Invalid SSH command")
+ }
+
+ return nil
+}
+
+func (s *Shell) isSshConnection() bool {
+ ok := os.Getenv("SSH_CONNECTION")
+ return ok != ""
+}
+
+func (s *Shell) isValidSshCommand() bool {
+ err := s.parseCommand(os.Getenv("SSH_ORIGINAL_COMMAND"))
+ return err == nil
+}
+
+func (s *Shell) parseWho() {
+ for _, argument := range s.Arguments {
+ if keyId := tryParseKeyId(argument); keyId != "" {
+ s.GitlabKeyId = keyId
+ break
+ }
+
+ if username := tryParseUsername(argument); username != "" {
+ s.GitlabUsername = username
+ break
+ }
+ }
+}
+
+func tryParseKeyId(argument string) string {
+ matchInfo := whoKeyRegex.FindStringSubmatch(argument)
+ if len(matchInfo) == 2 {
+ // The first element is the full matched string
+ // The second element is the named `keyid`
+ return matchInfo[1]
+ }
+
+ return ""
+}
+
+func tryParseUsername(argument string) string {
+ matchInfo := whoUsernameRegex.FindStringSubmatch(argument)
+ if len(matchInfo) == 2 {
+ // The first element is the full matched string
+ // The second element is the named `username`
+ return matchInfo[1]
+ }
+
+ return ""
+}
+
+func (s *Shell) parseCommand(commandString string) error {
+ args, err := shellwords.Parse(commandString)
+ if err != nil {
+ return err
+ }
+
+ // Handle Git for Windows 2.14 using "git upload-pack" instead of git-upload-pack
+ if len(args) > 1 && args[0] == "git" {
+ command := args[0] + "-" + args[1]
+ commandArgs := args[2:]
+
+ args = append([]string{command}, commandArgs...)
+ }
+
+ s.SshArgs = args
+
+ return nil
+}
+
+func (s *Shell) defineCommandType() {
+ if len(s.SshArgs) == 0 {
+ s.CommandType = Discover
+ } else {
+ s.CommandType = CommandType(s.SshArgs[0])
+ }
+}
diff --git a/internal/command/discover/discover.go b/internal/command/discover/discover.go
new file mode 100644
index 0000000..de94b56
--- /dev/null
+++ b/internal/command/discover/discover.go
@@ -0,0 +1,40 @@
+package discover
+
+import (
+ "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/config"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/discover"
+)
+
+type Command struct {
+ Config *config.Config
+ Args *commandargs.Shell
+ ReadWriter *readwriter.ReadWriter
+}
+
+func (c *Command) Execute() error {
+ response, err := c.getUserInfo()
+ if err != nil {
+ return fmt.Errorf("Failed to get username: %v", err)
+ }
+
+ if response.IsAnonymous() {
+ fmt.Fprintf(c.ReadWriter.Out, "Welcome to GitLab, Anonymous!\n")
+ } else {
+ fmt.Fprintf(c.ReadWriter.Out, "Welcome to GitLab, @%s!\n", response.Username)
+ }
+
+ return nil
+}
+
+func (c *Command) getUserInfo() (*discover.Response, error) {
+ client, err := discover.NewClient(c.Config)
+ if err != nil {
+ return nil, err
+ }
+
+ return client.GetByCommandArgs(c.Args)
+}
diff --git a/internal/command/discover/discover_test.go b/internal/command/discover/discover_test.go
new file mode 100644
index 0000000..7e052f7
--- /dev/null
+++ b/internal/command/discover/discover_test.go
@@ -0,0 +1,135 @@
+package discover
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "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/testserver"
+)
+
+var (
+ requests = []testserver.TestRequestHandler{
+ {
+ Path: "/api/v4/internal/discover",
+ Handler: func(w http.ResponseWriter, r *http.Request) {
+ if r.URL.Query().Get("key_id") == "1" || r.URL.Query().Get("username") == "alex-doe" {
+ body := map[string]interface{}{
+ "id": 2,
+ "username": "alex-doe",
+ "name": "Alex Doe",
+ }
+ json.NewEncoder(w).Encode(body)
+ } else if r.URL.Query().Get("username") == "broken_message" {
+ body := map[string]string{
+ "message": "Forbidden!",
+ }
+ w.WriteHeader(http.StatusForbidden)
+ json.NewEncoder(w).Encode(body)
+ } else if r.URL.Query().Get("username") == "broken" {
+ w.WriteHeader(http.StatusInternalServerError)
+ } else {
+ fmt.Fprint(w, "null")
+ }
+ },
+ },
+ }
+)
+
+func TestExecute(t *testing.T) {
+ url, cleanup := testserver.StartSocketHttpServer(t, requests)
+ defer cleanup()
+
+ testCases := []struct {
+ desc string
+ arguments *commandargs.Shell
+ expectedOutput string
+ }{
+ {
+ desc: "With a known username",
+ arguments: &commandargs.Shell{GitlabUsername: "alex-doe"},
+ expectedOutput: "Welcome to GitLab, @alex-doe!\n",
+ },
+ {
+ desc: "With a known key id",
+ arguments: &commandargs.Shell{GitlabKeyId: "1"},
+ expectedOutput: "Welcome to GitLab, @alex-doe!\n",
+ },
+ {
+ desc: "With an unknown key",
+ arguments: &commandargs.Shell{GitlabKeyId: "-1"},
+ expectedOutput: "Welcome to GitLab, Anonymous!\n",
+ },
+ {
+ desc: "With an unknown username",
+ arguments: &commandargs.Shell{GitlabUsername: "unknown"},
+ expectedOutput: "Welcome to GitLab, Anonymous!\n",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ buffer := &bytes.Buffer{}
+ cmd := &Command{
+ Config: &config.Config{GitlabUrl: url},
+ Args: tc.arguments,
+ ReadWriter: &readwriter.ReadWriter{Out: buffer},
+ }
+
+ err := cmd.Execute()
+
+ require.NoError(t, err)
+ require.Equal(t, tc.expectedOutput, buffer.String())
+ })
+ }
+}
+
+func TestFailingExecute(t *testing.T) {
+ url, cleanup := testserver.StartSocketHttpServer(t, requests)
+ defer cleanup()
+
+ testCases := []struct {
+ desc string
+ arguments *commandargs.Shell
+ expectedError string
+ }{
+ {
+ desc: "With missing arguments",
+ arguments: &commandargs.Shell{},
+ expectedError: "Failed to get username: who='' is invalid",
+ },
+ {
+ desc: "When the API returns an error",
+ arguments: &commandargs.Shell{GitlabUsername: "broken_message"},
+ expectedError: "Failed to get username: Forbidden!",
+ },
+ {
+ desc: "When the API fails",
+ arguments: &commandargs.Shell{GitlabUsername: "broken"},
+ expectedError: "Failed to get username: Internal API error (500)",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ buffer := &bytes.Buffer{}
+ cmd := &Command{
+ Config: &config.Config{GitlabUrl: url},
+ Args: tc.arguments,
+ ReadWriter: &readwriter.ReadWriter{Out: buffer},
+ }
+
+ err := cmd.Execute()
+
+ require.Empty(t, buffer.String())
+ require.EqualError(t, err, tc.expectedError)
+ })
+ }
+}
diff --git a/internal/command/healthcheck/healthcheck.go b/internal/command/healthcheck/healthcheck.go
new file mode 100644
index 0000000..fef981c
--- /dev/null
+++ b/internal/command/healthcheck/healthcheck.go
@@ -0,0 +1,49 @@
+package healthcheck
+
+import (
+ "fmt"
+
+ "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/healthcheck"
+)
+
+var (
+ apiMessage = "Internal API available"
+ redisMessage = "Redis available via internal API"
+)
+
+type Command struct {
+ Config *config.Config
+ ReadWriter *readwriter.ReadWriter
+}
+
+func (c *Command) Execute() error {
+ response, err := c.runCheck()
+ if err != nil {
+ return fmt.Errorf("%v: FAILED - %v", apiMessage, err)
+ }
+
+ fmt.Fprintf(c.ReadWriter.Out, "%v: OK\n", apiMessage)
+
+ if !response.Redis {
+ return fmt.Errorf("%v: FAILED", redisMessage)
+ }
+
+ fmt.Fprintf(c.ReadWriter.Out, "%v: OK\n", redisMessage)
+ return nil
+}
+
+func (c *Command) runCheck() (*healthcheck.Response, error) {
+ client, err := healthcheck.NewClient(c.Config)
+ if err != nil {
+ return nil, err
+ }
+
+ response, err := client.Check()
+ if err != nil {
+ return nil, err
+ }
+
+ return response, nil
+}
diff --git a/internal/command/healthcheck/healthcheck_test.go b/internal/command/healthcheck/healthcheck_test.go
new file mode 100644
index 0000000..6c92ebc
--- /dev/null
+++ b/internal/command/healthcheck/healthcheck_test.go
@@ -0,0 +1,90 @@
+package healthcheck
+
+import (
+ "bytes"
+ "encoding/json"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/require"
+
+ "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/healthcheck"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver"
+)
+
+var (
+ okResponse = &healthcheck.Response{
+ APIVersion: "v4",
+ GitlabVersion: "v12.0.0-ee",
+ GitlabRevision: "3b13818e8330f68625d80d9bf5d8049c41fbe197",
+ Redis: true,
+ }
+
+ badRedisResponse = &healthcheck.Response{Redis: false}
+
+ okHandlers = buildTestHandlers(200, okResponse)
+ badRedisHandlers = buildTestHandlers(200, badRedisResponse)
+ brokenHandlers = buildTestHandlers(500, nil)
+)
+
+func buildTestHandlers(code int, rsp *healthcheck.Response) []testserver.TestRequestHandler {
+ return []testserver.TestRequestHandler{
+ {
+ Path: "/api/v4/internal/check",
+ Handler: func(w http.ResponseWriter, r *http.Request) {
+ w.WriteHeader(code)
+ if rsp != nil {
+ json.NewEncoder(w).Encode(rsp)
+ }
+ },
+ },
+ }
+}
+
+func TestExecute(t *testing.T) {
+ url, cleanup := testserver.StartSocketHttpServer(t, okHandlers)
+ defer cleanup()
+
+ buffer := &bytes.Buffer{}
+ cmd := &Command{
+ Config: &config.Config{GitlabUrl: url},
+ ReadWriter: &readwriter.ReadWriter{Out: buffer},
+ }
+
+ err := cmd.Execute()
+
+ require.NoError(t, err)
+ require.Equal(t, "Internal API available: OK\nRedis available via internal API: OK\n", buffer.String())
+}
+
+func TestFailingRedisExecute(t *testing.T) {
+ url, cleanup := testserver.StartSocketHttpServer(t, badRedisHandlers)
+ defer cleanup()
+
+ buffer := &bytes.Buffer{}
+ cmd := &Command{
+ Config: &config.Config{GitlabUrl: url},
+ ReadWriter: &readwriter.ReadWriter{Out: buffer},
+ }
+
+ err := cmd.Execute()
+ require.Error(t, err, "Redis available via internal API: FAILED")
+ require.Equal(t, "Internal API available: OK\n", buffer.String())
+}
+
+func TestFailingAPIExecute(t *testing.T) {
+ url, cleanup := testserver.StartSocketHttpServer(t, brokenHandlers)
+ defer cleanup()
+
+ buffer := &bytes.Buffer{}
+ cmd := &Command{
+ Config: &config.Config{GitlabUrl: url},
+ ReadWriter: &readwriter.ReadWriter{Out: buffer},
+ }
+
+ err := cmd.Execute()
+ require.Empty(t, buffer.String())
+ require.EqualError(t, err, "Internal API available: FAILED - Internal API error (500)")
+}
diff --git a/internal/command/lfsauthenticate/lfsauthenticate.go b/internal/command/lfsauthenticate/lfsauthenticate.go
new file mode 100644
index 0000000..bff5e7f
--- /dev/null
+++ b/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.Shell
+ 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/internal/command/lfsauthenticate/lfsauthenticate_test.go b/internal/command/lfsauthenticate/lfsauthenticate_test.go
new file mode 100644
index 0000000..a6836a8
--- /dev/null
+++ b/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.Shell
+ expectedOutput string
+ }{
+ {
+ desc: "With missing arguments",
+ arguments: &commandargs.Shell{},
+ expectedOutput: "> GitLab: Disallowed command",
+ },
+ {
+ desc: "With disallowed command",
+ arguments: &commandargs.Shell{GitlabKeyId: "1", SshArgs: []string{"git-lfs-authenticate", "group/repo", "unknown"}},
+ expectedOutput: "> GitLab: Disallowed command",
+ },
+ {
+ desc: "With disallowed user",
+ arguments: &commandargs.Shell{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.Shell{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/internal/command/readwriter/readwriter.go b/internal/command/readwriter/readwriter.go
new file mode 100644
index 0000000..da18d30
--- /dev/null
+++ b/internal/command/readwriter/readwriter.go
@@ -0,0 +1,9 @@
+package readwriter
+
+import "io"
+
+type ReadWriter struct {
+ Out io.Writer
+ In io.Reader
+ ErrOut io.Writer
+}
diff --git a/internal/command/receivepack/customaction.go b/internal/command/receivepack/customaction.go
new file mode 100644
index 0000000..8623437
--- /dev/null
+++ b/internal/command/receivepack/customaction.go
@@ -0,0 +1,99 @@
+package receivepack
+
+import (
+ "bytes"
+ "errors"
+ "fmt"
+ "io"
+ "io/ioutil"
+ "net/http"
+ "strings"
+
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet"
+ "gitlab.com/gitlab-org/gitlab-shell/go/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")
+ }
+
+ c.displayInfoMessage(data.InfoMessage)
+
+ return c.processApiEndpoints(response)
+}
+
+func (c *Command) displayInfoMessage(infoMessage string) {
+ messages := strings.Split(infoMessage, "\n")
+
+ for _, msg := range messages {
+ fmt.Fprintf(c.ReadWriter.ErrOut, "> GitLab: %v\n", msg)
+ }
+}
+
+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
new file mode 100644
index 0000000..bd4991d
--- /dev/null
+++ b/internal/command/receivepack/customaction_test.go
@@ -0,0 +1,105 @@
+package receivepack
+
+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/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",
+ "info_message": "info_message\none more message",
+ },
+ },
+ }
+ 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, "> GitLab: info_message\n> GitLab: one more message\n", errBuf.String())
+ require.Equal(t, "customoutput", outBuf.String())
+}
diff --git a/internal/command/receivepack/gitalycall.go b/internal/command/receivepack/gitalycall.go
new file mode 100644
index 0000000..d735f17
--- /dev/null
+++ b/internal/command/receivepack/gitalycall.go
@@ -0,0 +1,39 @@
+package receivepack
+
+import (
+ "context"
+
+ "google.golang.org/grpc"
+
+ "gitlab.com/gitlab-org/gitaly/client"
+ pb "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/shared/accessverifier"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/handler"
+)
+
+func (c *Command) performGitalyCall(response *accessverifier.Response) error {
+ gc := &handler.GitalyCommand{
+ Config: c.Config,
+ ServiceName: string(commandargs.ReceivePack),
+ Address: response.Gitaly.Address,
+ Token: response.Gitaly.Token,
+ }
+
+ request := &pb.SSHReceivePackRequest{
+ Repository: &response.Gitaly.Repo,
+ GlId: response.UserId,
+ GlRepository: response.Repo,
+ GlUsername: response.Username,
+ GitProtocol: response.GitProtocol,
+ GitConfigOptions: response.GitConfigOptions,
+ }
+
+ return gc.RunGitalyCommand(func(ctx context.Context, conn *grpc.ClientConn) (int32, error) {
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ rw := c.ReadWriter
+ return client.ReceivePack(ctx, conn, rw.In, rw.Out, rw.ErrOut, request)
+ })
+}
diff --git a/internal/command/receivepack/gitalycall_test.go b/internal/command/receivepack/gitalycall_test.go
new file mode 100644
index 0000000..eac9218
--- /dev/null
+++ b/internal/command/receivepack/gitalycall_test.go
@@ -0,0 +1,40 @@
+package receivepack
+
+import (
+ "bytes"
+ "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/testserver"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper/requesthandlers"
+)
+
+func TestReceivePack(t *testing.T) {
+ gitalyAddress, cleanup := testserver.StartGitalyServer(t)
+ defer cleanup()
+
+ requests := requesthandlers.BuildAllowedWithGitalyHandlers(t, gitalyAddress)
+ url, cleanup := testserver.StartHttpServer(t, requests)
+ defer cleanup()
+
+ output := &bytes.Buffer{}
+ input := &bytes.Buffer{}
+
+ userId := "1"
+ repo := "group/repo"
+
+ cmd := &Command{
+ Config: &config.Config{GitlabUrl: url},
+ Args: &commandargs.Shell{GitlabKeyId: userId, CommandType: commandargs.ReceivePack, SshArgs: []string{"git-receive-pack", repo}},
+ ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output, In: input},
+ }
+
+ err := cmd.Execute()
+ require.NoError(t, err)
+
+ require.Equal(t, "ReceivePack: "+userId+" "+repo, output.String())
+}
diff --git a/internal/command/receivepack/receivepack.go b/internal/command/receivepack/receivepack.go
new file mode 100644
index 0000000..eb0b2fe
--- /dev/null
+++ b/internal/command/receivepack/receivepack.go
@@ -0,0 +1,40 @@
+package receivepack
+
+import (
+ "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"
+)
+
+type Command struct {
+ Config *config.Config
+ Args *commandargs.Shell
+ ReadWriter *readwriter.ReadWriter
+}
+
+func (c *Command) Execute() error {
+ args := c.Args.SshArgs
+ if len(args) != 2 {
+ return disallowedcommand.Error
+ }
+
+ repo := args[1]
+ response, err := c.verifyAccess(repo)
+ if err != nil {
+ return err
+ }
+
+ if response.IsCustomAction() {
+ return c.processCustomAction(response)
+ }
+
+ return c.performGitalyCall(response)
+}
+
+func (c *Command) verifyAccess(repo string) (*accessverifier.Response, error) {
+ cmd := accessverifier.Command{c.Config, c.Args, c.ReadWriter}
+
+ return cmd.Verify(c.Args.CommandType, repo)
+}
diff --git a/internal/command/receivepack/receivepack_test.go b/internal/command/receivepack/receivepack_test.go
new file mode 100644
index 0000000..a45d054
--- /dev/null
+++ b/internal/command/receivepack/receivepack_test.go
@@ -0,0 +1,32 @@
+package receivepack
+
+import (
+ "bytes"
+ "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/testserver"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper/requesthandlers"
+)
+
+func TestForbiddenAccess(t *testing.T) {
+ requests := requesthandlers.BuildDisallowedByApiHandlers(t)
+ url, cleanup := testserver.StartHttpServer(t, requests)
+ defer cleanup()
+
+ 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"}},
+ ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output, In: input},
+ }
+
+ err := cmd.Execute()
+ require.Equal(t, "Disallowed by API call", err.Error())
+}
diff --git a/internal/command/shared/accessverifier/accessverifier.go b/internal/command/shared/accessverifier/accessverifier.go
new file mode 100644
index 0000000..fc6fa17
--- /dev/null
+++ b/internal/command/shared/accessverifier/accessverifier.go
@@ -0,0 +1,45 @@
+package accessverifier
+
+import (
+ "errors"
+ "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/config"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/accessverifier"
+)
+
+type Response = accessverifier.Response
+
+type Command struct {
+ Config *config.Config
+ Args *commandargs.Shell
+ ReadWriter *readwriter.ReadWriter
+}
+
+func (c *Command) Verify(action commandargs.CommandType, repo string) (*Response, error) {
+ client, err := accessverifier.NewClient(c.Config)
+ if err != nil {
+ return nil, err
+ }
+
+ response, err := client.Verify(c.Args, action, repo)
+ if err != nil {
+ return nil, err
+ }
+
+ c.displayConsoleMessages(response.ConsoleMessages)
+
+ if !response.Success {
+ return nil, errors.New(response.Message)
+ }
+
+ return response, nil
+}
+
+func (c *Command) displayConsoleMessages(messages []string) {
+ for _, msg := range messages {
+ fmt.Fprintf(c.ReadWriter.ErrOut, "> GitLab: %v\n", msg)
+ }
+}
diff --git a/internal/command/shared/accessverifier/accessverifier_test.go b/internal/command/shared/accessverifier/accessverifier_test.go
new file mode 100644
index 0000000..c19ed37
--- /dev/null
+++ b/internal/command/shared/accessverifier/accessverifier_test.go
@@ -0,0 +1,82 @@
+package accessverifier
+
+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/testserver"
+)
+
+var (
+ repo = "group/repo"
+ action = commandargs.ReceivePack
+)
+
+func setup(t *testing.T) (*Command, *bytes.Buffer, *bytes.Buffer, func()) {
+ 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 *accessverifier.Request
+ err = json.Unmarshal(b, &requestBody)
+ require.NoError(t, err)
+
+ if requestBody.KeyId == "1" {
+ body := map[string]interface{}{
+ "gl_console_messages": []string{"console", "message"},
+ }
+ require.NoError(t, json.NewEncoder(w).Encode(body))
+ } else {
+ body := map[string]interface{}{
+ "status": false,
+ "message": "missing user",
+ }
+ require.NoError(t, json.NewEncoder(w).Encode(body))
+ }
+ },
+ },
+ }
+
+ url, cleanup := testserver.StartSocketHttpServer(t, requests)
+
+ errBuf := &bytes.Buffer{}
+ outBuf := &bytes.Buffer{}
+
+ readWriter := &readwriter.ReadWriter{Out: outBuf, ErrOut: errBuf}
+ cmd := &Command{Config: &config.Config{GitlabUrl: url}, ReadWriter: readWriter}
+
+ return cmd, errBuf, outBuf, cleanup
+}
+
+func TestMissingUser(t *testing.T) {
+ cmd, _, _, cleanup := setup(t)
+ defer cleanup()
+
+ cmd.Args = &commandargs.Shell{GitlabKeyId: "2"}
+ _, err := cmd.Verify(action, repo)
+
+ require.Equal(t, "missing user", err.Error())
+}
+
+func TestConsoleMessages(t *testing.T) {
+ cmd, errBuf, outBuf, cleanup := setup(t)
+ defer cleanup()
+
+ cmd.Args = &commandargs.Shell{GitlabKeyId: "1"}
+ cmd.Verify(action, repo)
+
+ require.Equal(t, "> GitLab: console\n> GitLab: message\n", errBuf.String())
+ require.Empty(t, outBuf.String())
+}
diff --git a/internal/command/shared/disallowedcommand/disallowedcommand.go b/internal/command/shared/disallowedcommand/disallowedcommand.go
new file mode 100644
index 0000000..3c98bcc
--- /dev/null
+++ b/internal/command/shared/disallowedcommand/disallowedcommand.go
@@ -0,0 +1,7 @@
+package disallowedcommand
+
+import "errors"
+
+var (
+ Error = errors.New("> GitLab: Disallowed command")
+)
diff --git a/internal/command/twofactorrecover/twofactorrecover.go b/internal/command/twofactorrecover/twofactorrecover.go
new file mode 100644
index 0000000..c68080a
--- /dev/null
+++ b/internal/command/twofactorrecover/twofactorrecover.go
@@ -0,0 +1,65 @@
+package twofactorrecover
+
+import (
+ "fmt"
+ "strings"
+
+ "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/twofactorrecover"
+)
+
+type Command struct {
+ Config *config.Config
+ Args *commandargs.Shell
+ ReadWriter *readwriter.ReadWriter
+}
+
+func (c *Command) Execute() error {
+ if c.canContinue() {
+ c.displayRecoveryCodes()
+ } else {
+ fmt.Fprintln(c.ReadWriter.Out, "\nNew recovery codes have *not* been generated. Existing codes will remain valid.")
+ }
+
+ return nil
+}
+
+func (c *Command) canContinue() bool {
+ question :=
+ "Are you sure you want to generate new two-factor recovery codes?\n" +
+ "Any existing recovery codes you saved will be invalidated. (yes/no)"
+ fmt.Fprintln(c.ReadWriter.Out, question)
+
+ var answer string
+ fmt.Fscanln(c.ReadWriter.In, &answer)
+
+ return answer == "yes"
+}
+
+func (c *Command) displayRecoveryCodes() {
+ codes, err := c.getRecoveryCodes()
+
+ if err == nil {
+ messageWithCodes :=
+ "\nYour two-factor authentication recovery codes are:\n\n" +
+ strings.Join(codes, "\n") +
+ "\n\nDuring sign in, use one of the codes above when prompted for\n" +
+ "your two-factor code. Then, visit your Profile Settings and add\n" +
+ "a new device so you do not lose access to your account again.\n"
+ fmt.Fprint(c.ReadWriter.Out, messageWithCodes)
+ } else {
+ fmt.Fprintf(c.ReadWriter.Out, "\nAn error occurred while trying to generate new recovery codes.\n%v\n", err)
+ }
+}
+
+func (c *Command) getRecoveryCodes() ([]string, error) {
+ client, err := twofactorrecover.NewClient(c.Config)
+
+ if err != nil {
+ return nil, err
+ }
+
+ return client.GetRecoveryCodes(c.Args)
+}
diff --git a/internal/command/twofactorrecover/twofactorrecover_test.go b/internal/command/twofactorrecover/twofactorrecover_test.go
new file mode 100644
index 0000000..291d499
--- /dev/null
+++ b/internal/command/twofactorrecover/twofactorrecover_test.go
@@ -0,0 +1,136 @@
+package twofactorrecover
+
+import (
+ "bytes"
+ "encoding/json"
+ "io/ioutil"
+ "net/http"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "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/testserver"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/twofactorrecover"
+)
+
+var (
+ requests []testserver.TestRequestHandler
+)
+
+func setup(t *testing.T) {
+ requests = []testserver.TestRequestHandler{
+ {
+ Path: "/api/v4/internal/two_factor_recovery_codes",
+ Handler: func(w http.ResponseWriter, r *http.Request) {
+ b, err := ioutil.ReadAll(r.Body)
+ defer r.Body.Close()
+
+ require.NoError(t, err)
+
+ var requestBody *twofactorrecover.RequestBody
+ json.Unmarshal(b, &requestBody)
+
+ switch requestBody.KeyId {
+ case "1":
+ body := map[string]interface{}{
+ "success": true,
+ "recovery_codes": [2]string{"recovery", "codes"},
+ }
+ json.NewEncoder(w).Encode(body)
+ case "forbidden":
+ body := map[string]interface{}{
+ "success": false,
+ "message": "Forbidden!",
+ }
+ json.NewEncoder(w).Encode(body)
+ case "broken":
+ w.WriteHeader(http.StatusInternalServerError)
+ }
+ },
+ },
+ }
+}
+
+const (
+ question = "Are you sure you want to generate new two-factor recovery codes?\n" +
+ "Any existing recovery codes you saved will be invalidated. (yes/no)\n\n"
+ errorHeader = "An error occurred while trying to generate new recovery codes.\n"
+)
+
+func TestExecute(t *testing.T) {
+ setup(t)
+
+ url, cleanup := testserver.StartSocketHttpServer(t, requests)
+ defer cleanup()
+
+ testCases := []struct {
+ desc string
+ arguments *commandargs.Shell
+ answer string
+ expectedOutput string
+ }{
+ {
+ desc: "With a known key id",
+ arguments: &commandargs.Shell{GitlabKeyId: "1"},
+ answer: "yes\n",
+ expectedOutput: question +
+ "Your two-factor authentication recovery codes are:\n\nrecovery\ncodes\n\n" +
+ "During sign in, use one of the codes above when prompted for\n" +
+ "your two-factor code. Then, visit your Profile Settings and add\n" +
+ "a new device so you do not lose access to your account again.\n",
+ },
+ {
+ desc: "With bad response",
+ arguments: &commandargs.Shell{GitlabKeyId: "-1"},
+ answer: "yes\n",
+ expectedOutput: question + errorHeader + "Parsing failed\n",
+ },
+ {
+ desc: "With API returns an error",
+ arguments: &commandargs.Shell{GitlabKeyId: "forbidden"},
+ answer: "yes\n",
+ expectedOutput: question + errorHeader + "Forbidden!\n",
+ },
+ {
+ desc: "With API fails",
+ arguments: &commandargs.Shell{GitlabKeyId: "broken"},
+ answer: "yes\n",
+ expectedOutput: question + errorHeader + "Internal API error (500)\n",
+ },
+ {
+ desc: "With missing arguments",
+ arguments: &commandargs.Shell{},
+ answer: "yes\n",
+ expectedOutput: question + errorHeader + "who='' is invalid\n",
+ },
+ {
+ desc: "With negative answer",
+ arguments: &commandargs.Shell{},
+ answer: "no\n",
+ expectedOutput: question +
+ "New recovery codes have *not* been generated. Existing codes will remain valid.\n",
+ },
+ }
+
+ for _, tc := range testCases {
+ t.Run(tc.desc, func(t *testing.T) {
+ output := &bytes.Buffer{}
+ input := bytes.NewBufferString(tc.answer)
+
+ cmd := &Command{
+ Config: &config.Config{GitlabUrl: url},
+ Args: tc.arguments,
+ ReadWriter: &readwriter.ReadWriter{Out: output, In: input},
+ }
+
+ err := cmd.Execute()
+
+ assert.NoError(t, err)
+ assert.Equal(t, tc.expectedOutput, output.String())
+ })
+ }
+}
diff --git a/internal/command/uploadarchive/gitalycall.go b/internal/command/uploadarchive/gitalycall.go
new file mode 100644
index 0000000..e810ba3
--- /dev/null
+++ b/internal/command/uploadarchive/gitalycall.go
@@ -0,0 +1,32 @@
+package uploadarchive
+
+import (
+ "context"
+
+ "google.golang.org/grpc"
+
+ "gitlab.com/gitlab-org/gitaly/client"
+ pb "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/accessverifier"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/handler"
+)
+
+func (c *Command) performGitalyCall(response *accessverifier.Response) error {
+ gc := &handler.GitalyCommand{
+ Config: c.Config,
+ ServiceName: string(commandargs.UploadArchive),
+ Address: response.Gitaly.Address,
+ Token: response.Gitaly.Token,
+ }
+
+ request := &pb.SSHUploadArchiveRequest{Repository: &response.Gitaly.Repo}
+
+ return gc.RunGitalyCommand(func(ctx context.Context, conn *grpc.ClientConn) (int32, error) {
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ rw := c.ReadWriter
+ return client.UploadArchive(ctx, conn, rw.In, rw.Out, rw.ErrOut, request)
+ })
+}
diff --git a/internal/command/uploadarchive/gitalycall_test.go b/internal/command/uploadarchive/gitalycall_test.go
new file mode 100644
index 0000000..5eb2eae
--- /dev/null
+++ b/internal/command/uploadarchive/gitalycall_test.go
@@ -0,0 +1,40 @@
+package uploadarchive
+
+import (
+ "bytes"
+ "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/testserver"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper/requesthandlers"
+)
+
+func TestUploadPack(t *testing.T) {
+ gitalyAddress, cleanup := testserver.StartGitalyServer(t)
+ defer cleanup()
+
+ requests := requesthandlers.BuildAllowedWithGitalyHandlers(t, gitalyAddress)
+ url, cleanup := testserver.StartHttpServer(t, requests)
+ defer cleanup()
+
+ output := &bytes.Buffer{}
+ input := &bytes.Buffer{}
+
+ userId := "1"
+ repo := "group/repo"
+
+ cmd := &Command{
+ Config: &config.Config{GitlabUrl: url},
+ Args: &commandargs.Shell{GitlabKeyId: userId, CommandType: commandargs.UploadArchive, SshArgs: []string{"git-upload-archive", repo}},
+ ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output, In: input},
+ }
+
+ err := cmd.Execute()
+ require.NoError(t, err)
+
+ require.Equal(t, "UploadArchive: "+repo, output.String())
+}
diff --git a/internal/command/uploadarchive/uploadarchive.go b/internal/command/uploadarchive/uploadarchive.go
new file mode 100644
index 0000000..2846455
--- /dev/null
+++ b/internal/command/uploadarchive/uploadarchive.go
@@ -0,0 +1,36 @@
+package uploadarchive
+
+import (
+ "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"
+)
+
+type Command struct {
+ Config *config.Config
+ Args *commandargs.Shell
+ ReadWriter *readwriter.ReadWriter
+}
+
+func (c *Command) Execute() error {
+ args := c.Args.SshArgs
+ if len(args) != 2 {
+ return disallowedcommand.Error
+ }
+
+ repo := args[1]
+ response, err := c.verifyAccess(repo)
+ if err != nil {
+ return err
+ }
+
+ return c.performGitalyCall(response)
+}
+
+func (c *Command) verifyAccess(repo string) (*accessverifier.Response, error) {
+ cmd := accessverifier.Command{c.Config, c.Args, c.ReadWriter}
+
+ return cmd.Verify(c.Args.CommandType, repo)
+}
diff --git a/internal/command/uploadarchive/uploadarchive_test.go b/internal/command/uploadarchive/uploadarchive_test.go
new file mode 100644
index 0000000..4cd6832
--- /dev/null
+++ b/internal/command/uploadarchive/uploadarchive_test.go
@@ -0,0 +1,31 @@
+package uploadarchive
+
+import (
+ "bytes"
+ "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/testserver"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper/requesthandlers"
+)
+
+func TestForbiddenAccess(t *testing.T) {
+ requests := requesthandlers.BuildDisallowedByApiHandlers(t)
+ url, cleanup := testserver.StartHttpServer(t, requests)
+ defer cleanup()
+
+ output := &bytes.Buffer{}
+
+ cmd := &Command{
+ Config: &config.Config{GitlabUrl: url},
+ Args: &commandargs.Shell{GitlabKeyId: "disallowed", SshArgs: []string{"git-upload-archive", "group/repo"}},
+ ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output},
+ }
+
+ err := cmd.Execute()
+ require.Equal(t, "Disallowed by API call", err.Error())
+}
diff --git a/internal/command/uploadpack/gitalycall.go b/internal/command/uploadpack/gitalycall.go
new file mode 100644
index 0000000..5dff24a
--- /dev/null
+++ b/internal/command/uploadpack/gitalycall.go
@@ -0,0 +1,36 @@
+package uploadpack
+
+import (
+ "context"
+
+ "google.golang.org/grpc"
+
+ "gitlab.com/gitlab-org/gitaly/client"
+ pb "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/accessverifier"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/handler"
+)
+
+func (c *Command) performGitalyCall(response *accessverifier.Response) error {
+ gc := &handler.GitalyCommand{
+ Config: c.Config,
+ ServiceName: string(commandargs.UploadPack),
+ Address: response.Gitaly.Address,
+ Token: response.Gitaly.Token,
+ }
+
+ request := &pb.SSHUploadPackRequest{
+ Repository: &response.Gitaly.Repo,
+ GitProtocol: response.GitProtocol,
+ GitConfigOptions: response.GitConfigOptions,
+ }
+
+ return gc.RunGitalyCommand(func(ctx context.Context, conn *grpc.ClientConn) (int32, error) {
+ ctx, cancel := context.WithCancel(ctx)
+ defer cancel()
+
+ rw := c.ReadWriter
+ return client.UploadPack(ctx, conn, rw.In, rw.Out, rw.ErrOut, request)
+ })
+}
diff --git a/internal/command/uploadpack/gitalycall_test.go b/internal/command/uploadpack/gitalycall_test.go
new file mode 100644
index 0000000..eb18aa8
--- /dev/null
+++ b/internal/command/uploadpack/gitalycall_test.go
@@ -0,0 +1,40 @@
+package uploadpack
+
+import (
+ "bytes"
+ "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/testserver"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper/requesthandlers"
+)
+
+func TestUploadPack(t *testing.T) {
+ gitalyAddress, cleanup := testserver.StartGitalyServer(t)
+ defer cleanup()
+
+ requests := requesthandlers.BuildAllowedWithGitalyHandlers(t, gitalyAddress)
+ url, cleanup := testserver.StartHttpServer(t, requests)
+ defer cleanup()
+
+ output := &bytes.Buffer{}
+ input := &bytes.Buffer{}
+
+ userId := "1"
+ repo := "group/repo"
+
+ cmd := &Command{
+ Config: &config.Config{GitlabUrl: url},
+ Args: &commandargs.Shell{GitlabKeyId: userId, CommandType: commandargs.UploadPack, SshArgs: []string{"git-upload-pack", repo}},
+ ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output, In: input},
+ }
+
+ err := cmd.Execute()
+ require.NoError(t, err)
+
+ require.Equal(t, "UploadPack: "+repo, output.String())
+}
diff --git a/internal/command/uploadpack/uploadpack.go b/internal/command/uploadpack/uploadpack.go
new file mode 100644
index 0000000..4b08bf2
--- /dev/null
+++ b/internal/command/uploadpack/uploadpack.go
@@ -0,0 +1,36 @@
+package uploadpack
+
+import (
+ "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"
+)
+
+type Command struct {
+ Config *config.Config
+ Args *commandargs.Shell
+ ReadWriter *readwriter.ReadWriter
+}
+
+func (c *Command) Execute() error {
+ args := c.Args.SshArgs
+ if len(args) != 2 {
+ return disallowedcommand.Error
+ }
+
+ repo := args[1]
+ response, err := c.verifyAccess(repo)
+ if err != nil {
+ return err
+ }
+
+ return c.performGitalyCall(response)
+}
+
+func (c *Command) verifyAccess(repo string) (*accessverifier.Response, error) {
+ cmd := accessverifier.Command{c.Config, c.Args, c.ReadWriter}
+
+ return cmd.Verify(c.Args.CommandType, repo)
+}
diff --git a/internal/command/uploadpack/uploadpack_test.go b/internal/command/uploadpack/uploadpack_test.go
new file mode 100644
index 0000000..27a0786
--- /dev/null
+++ b/internal/command/uploadpack/uploadpack_test.go
@@ -0,0 +1,31 @@
+package uploadpack
+
+import (
+ "bytes"
+ "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/testserver"
+ "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper/requesthandlers"
+)
+
+func TestForbiddenAccess(t *testing.T) {
+ requests := requesthandlers.BuildDisallowedByApiHandlers(t)
+ url, cleanup := testserver.StartHttpServer(t, requests)
+ defer cleanup()
+
+ output := &bytes.Buffer{}
+
+ cmd := &Command{
+ Config: &config.Config{GitlabUrl: url},
+ Args: &commandargs.Shell{GitlabKeyId: "disallowed", SshArgs: []string{"git-upload-pack", "group/repo"}},
+ ReadWriter: &readwriter.ReadWriter{ErrOut: output, Out: output},
+ }
+
+ err := cmd.Execute()
+ require.Equal(t, "Disallowed by API call", err.Error())
+}