From 83d11f4deeb20b852a0af3433190a0f7250a0027 Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Thu, 17 Oct 2019 12:04:52 +0100 Subject: Move go code up one level --- cmd/check/main.go | 42 ++++ cmd/gitlab-shell-authorized-keys-check/main.go | 44 ++++ .../main.go | 44 ++++ cmd/gitlab-shell/main.go | 44 ++++ go.mod | 16 ++ go.sum | 184 ++++++++++++++++ go/README.md | 18 -- go/cmd/check/main.go | 42 ---- go/cmd/gitlab-shell-authorized-keys-check/main.go | 44 ---- .../main.go | 44 ---- go/cmd/gitlab-shell/main.go | 44 ---- go/go.mod | 16 -- go/go.sum | 184 ---------------- .../command/authorizedkeys/authorized_keys.go | 61 ------ .../command/authorizedkeys/authorized_keys_test.go | 90 -------- .../authorizedprincipals/authorized_principals.go | 47 ----- .../authorized_principals_test.go | 47 ----- go/internal/command/command.go | 81 -------- go/internal/command/command_test.go | 146 ------------- go/internal/command/commandargs/authorized_keys.go | 51 ----- .../command/commandargs/authorized_principals.go | 50 ----- go/internal/command/commandargs/command_args.go | 31 --- .../command/commandargs/command_args_test.go | 231 --------------------- go/internal/command/commandargs/generic_args.go | 14 -- go/internal/command/commandargs/shell.go | 131 ------------ go/internal/command/discover/discover.go | 40 ---- go/internal/command/discover/discover_test.go | 135 ------------ go/internal/command/healthcheck/healthcheck.go | 49 ----- .../command/healthcheck/healthcheck_test.go | 90 -------- .../command/lfsauthenticate/lfsauthenticate.go | 104 ---------- .../lfsauthenticate/lfsauthenticate_test.go | 153 -------------- go/internal/command/readwriter/readwriter.go | 9 - go/internal/command/receivepack/customaction.go | 99 --------- .../command/receivepack/customaction_test.go | 105 ---------- go/internal/command/receivepack/gitalycall.go | 39 ---- go/internal/command/receivepack/gitalycall_test.go | 40 ---- go/internal/command/receivepack/receivepack.go | 40 ---- .../command/receivepack/receivepack_test.go | 32 --- .../shared/accessverifier/accessverifier.go | 45 ---- .../shared/accessverifier/accessverifier_test.go | 82 -------- .../shared/disallowedcommand/disallowedcommand.go | 7 - .../command/twofactorrecover/twofactorrecover.go | 65 ------ .../twofactorrecover/twofactorrecover_test.go | 136 ------------ go/internal/command/uploadarchive/gitalycall.go | 32 --- .../command/uploadarchive/gitalycall_test.go | 40 ---- go/internal/command/uploadarchive/uploadarchive.go | 36 ---- .../command/uploadarchive/uploadarchive_test.go | 31 --- go/internal/command/uploadpack/gitalycall.go | 36 ---- go/internal/command/uploadpack/gitalycall_test.go | 40 ---- go/internal/command/uploadpack/uploadpack.go | 36 ---- go/internal/command/uploadpack/uploadpack_test.go | 31 --- go/internal/config/config.go | 123 ----------- go/internal/config/config_test.go | 112 ---------- go/internal/config/httpclient.go | 122 ----------- go/internal/config/httpclient_test.go | 22 -- go/internal/executable/executable.go | 60 ------ go/internal/executable/executable_test.go | 104 ---------- go/internal/gitlabnet/accessverifier/client.go | 115 ---------- .../gitlabnet/accessverifier/client_test.go | 209 ------------------- go/internal/gitlabnet/authorizedkeys/client.go | 65 ------ .../gitlabnet/authorizedkeys/client_test.go | 105 ---------- go/internal/gitlabnet/client.go | 132 ------------ go/internal/gitlabnet/client_test.go | 219 ------------------- go/internal/gitlabnet/discover/client.go | 71 ------- go/internal/gitlabnet/discover/client_test.go | 137 ------------ go/internal/gitlabnet/healthcheck/client.go | 54 ----- go/internal/gitlabnet/healthcheck/client_test.go | 48 ----- go/internal/gitlabnet/httpclient_test.go | 96 --------- go/internal/gitlabnet/httpsclient_test.go | 125 ----------- go/internal/gitlabnet/lfsauthenticate/client.go | 66 ------ .../gitlabnet/lfsauthenticate/client_test.go | 117 ----------- go/internal/gitlabnet/testserver/gitalyserver.go | 78 ------- go/internal/gitlabnet/testserver/testserver.go | 82 -------- go/internal/gitlabnet/twofactorrecover/client.go | 89 -------- .../gitlabnet/twofactorrecover/client_test.go | 158 -------------- go/internal/handler/exec.go | 96 --------- go/internal/handler/exec_test.go | 42 ---- go/internal/keyline/key_line.go | 62 ------ go/internal/keyline/key_line_test.go | 82 -------- go/internal/logger/logger.go | 82 -------- go/internal/sshenv/sshenv.go | 15 -- go/internal/sshenv/sshenv_test.go | 20 -- .../testhelper/requesthandlers/requesthandlers.go | 58 ------ .../testdata/testroot/.gitlab_shell_secret | 1 - .../testdata/testroot/certs/invalid/server.crt | 10 - .../testdata/testroot/certs/valid/dir/.gitkeep | 0 .../testdata/testroot/certs/valid/server.crt | 22 -- .../testdata/testroot/certs/valid/server.key | 27 --- .../testhelper/testdata/testroot/config.yml | 0 .../testdata/testroot/custom/my-contents-is-secret | 1 - .../testhelper/testdata/testroot/gitlab-shell.log | 0 .../testdata/testroot/responses/allowed.json | 22 -- .../testroot/responses/allowed_with_payload.json | 31 --- go/internal/testhelper/testhelper.go | 93 --------- internal/command/authorizedkeys/authorized_keys.go | 61 ++++++ .../command/authorizedkeys/authorized_keys_test.go | 90 ++++++++ .../authorizedprincipals/authorized_principals.go | 47 +++++ .../authorized_principals_test.go | 47 +++++ internal/command/command.go | 81 ++++++++ internal/command/command_test.go | 146 +++++++++++++ internal/command/commandargs/authorized_keys.go | 51 +++++ .../command/commandargs/authorized_principals.go | 50 +++++ internal/command/commandargs/command_args.go | 31 +++ internal/command/commandargs/command_args_test.go | 231 +++++++++++++++++++++ internal/command/commandargs/generic_args.go | 14 ++ internal/command/commandargs/shell.go | 131 ++++++++++++ internal/command/discover/discover.go | 40 ++++ internal/command/discover/discover_test.go | 135 ++++++++++++ internal/command/healthcheck/healthcheck.go | 49 +++++ internal/command/healthcheck/healthcheck_test.go | 90 ++++++++ .../command/lfsauthenticate/lfsauthenticate.go | 104 ++++++++++ .../lfsauthenticate/lfsauthenticate_test.go | 153 ++++++++++++++ internal/command/readwriter/readwriter.go | 9 + internal/command/receivepack/customaction.go | 99 +++++++++ internal/command/receivepack/customaction_test.go | 105 ++++++++++ internal/command/receivepack/gitalycall.go | 39 ++++ internal/command/receivepack/gitalycall_test.go | 40 ++++ internal/command/receivepack/receivepack.go | 40 ++++ internal/command/receivepack/receivepack_test.go | 32 +++ .../shared/accessverifier/accessverifier.go | 45 ++++ .../shared/accessverifier/accessverifier_test.go | 82 ++++++++ .../shared/disallowedcommand/disallowedcommand.go | 7 + .../command/twofactorrecover/twofactorrecover.go | 65 ++++++ .../twofactorrecover/twofactorrecover_test.go | 136 ++++++++++++ internal/command/uploadarchive/gitalycall.go | 32 +++ internal/command/uploadarchive/gitalycall_test.go | 40 ++++ internal/command/uploadarchive/uploadarchive.go | 36 ++++ .../command/uploadarchive/uploadarchive_test.go | 31 +++ internal/command/uploadpack/gitalycall.go | 36 ++++ internal/command/uploadpack/gitalycall_test.go | 40 ++++ internal/command/uploadpack/uploadpack.go | 36 ++++ internal/command/uploadpack/uploadpack_test.go | 31 +++ internal/config/config.go | 123 +++++++++++ internal/config/config_test.go | 112 ++++++++++ internal/config/httpclient.go | 122 +++++++++++ internal/config/httpclient_test.go | 22 ++ internal/executable/executable.go | 60 ++++++ internal/executable/executable_test.go | 104 ++++++++++ internal/gitlabnet/accessverifier/client.go | 115 ++++++++++ internal/gitlabnet/accessverifier/client_test.go | 209 +++++++++++++++++++ internal/gitlabnet/authorizedkeys/client.go | 65 ++++++ internal/gitlabnet/authorizedkeys/client_test.go | 105 ++++++++++ internal/gitlabnet/client.go | 132 ++++++++++++ internal/gitlabnet/client_test.go | 219 +++++++++++++++++++ internal/gitlabnet/discover/client.go | 71 +++++++ internal/gitlabnet/discover/client_test.go | 137 ++++++++++++ internal/gitlabnet/healthcheck/client.go | 54 +++++ internal/gitlabnet/healthcheck/client_test.go | 48 +++++ internal/gitlabnet/httpclient_test.go | 96 +++++++++ internal/gitlabnet/httpsclient_test.go | 125 +++++++++++ internal/gitlabnet/lfsauthenticate/client.go | 66 ++++++ internal/gitlabnet/lfsauthenticate/client_test.go | 117 +++++++++++ internal/gitlabnet/testserver/gitalyserver.go | 78 +++++++ internal/gitlabnet/testserver/testserver.go | 82 ++++++++ internal/gitlabnet/twofactorrecover/client.go | 89 ++++++++ internal/gitlabnet/twofactorrecover/client_test.go | 158 ++++++++++++++ internal/handler/exec.go | 96 +++++++++ internal/handler/exec_test.go | 42 ++++ internal/keyline/key_line.go | 62 ++++++ internal/keyline/key_line_test.go | 82 ++++++++ internal/logger/logger.go | 82 ++++++++ internal/sshenv/sshenv.go | 15 ++ internal/sshenv/sshenv_test.go | 20 ++ .../testhelper/requesthandlers/requesthandlers.go | 58 ++++++ .../testdata/testroot/.gitlab_shell_secret | 1 + .../testdata/testroot/certs/invalid/server.crt | 10 + .../testdata/testroot/certs/valid/dir/.gitkeep | 0 .../testdata/testroot/certs/valid/server.crt | 22 ++ .../testdata/testroot/certs/valid/server.key | 27 +++ internal/testhelper/testdata/testroot/config.yml | 0 .../testdata/testroot/custom/my-contents-is-secret | 1 + .../testhelper/testdata/testroot/gitlab-shell.log | 0 .../testdata/testroot/responses/allowed.json | 22 ++ .../testroot/responses/allowed_with_payload.json | 31 +++ internal/testhelper/testhelper.go | 93 +++++++++ 175 files changed, 6079 insertions(+), 6097 deletions(-) create mode 100644 cmd/check/main.go create mode 100644 cmd/gitlab-shell-authorized-keys-check/main.go create mode 100644 cmd/gitlab-shell-authorized-principals-check/main.go create mode 100644 cmd/gitlab-shell/main.go create mode 100644 go.mod create mode 100644 go.sum delete mode 100644 go/README.md delete mode 100644 go/cmd/check/main.go delete mode 100644 go/cmd/gitlab-shell-authorized-keys-check/main.go delete mode 100644 go/cmd/gitlab-shell-authorized-principals-check/main.go delete mode 100644 go/cmd/gitlab-shell/main.go delete mode 100644 go/go.mod delete mode 100644 go/go.sum delete mode 100644 go/internal/command/authorizedkeys/authorized_keys.go delete mode 100644 go/internal/command/authorizedkeys/authorized_keys_test.go delete mode 100644 go/internal/command/authorizedprincipals/authorized_principals.go delete mode 100644 go/internal/command/authorizedprincipals/authorized_principals_test.go delete mode 100644 go/internal/command/command.go delete mode 100644 go/internal/command/command_test.go delete mode 100644 go/internal/command/commandargs/authorized_keys.go delete mode 100644 go/internal/command/commandargs/authorized_principals.go delete mode 100644 go/internal/command/commandargs/command_args.go delete mode 100644 go/internal/command/commandargs/command_args_test.go delete mode 100644 go/internal/command/commandargs/generic_args.go delete mode 100644 go/internal/command/commandargs/shell.go delete mode 100644 go/internal/command/discover/discover.go delete mode 100644 go/internal/command/discover/discover_test.go delete mode 100644 go/internal/command/healthcheck/healthcheck.go delete mode 100644 go/internal/command/healthcheck/healthcheck_test.go delete mode 100644 go/internal/command/lfsauthenticate/lfsauthenticate.go delete mode 100644 go/internal/command/lfsauthenticate/lfsauthenticate_test.go delete mode 100644 go/internal/command/readwriter/readwriter.go delete mode 100644 go/internal/command/receivepack/customaction.go delete mode 100644 go/internal/command/receivepack/customaction_test.go delete mode 100644 go/internal/command/receivepack/gitalycall.go delete mode 100644 go/internal/command/receivepack/gitalycall_test.go delete mode 100644 go/internal/command/receivepack/receivepack.go delete mode 100644 go/internal/command/receivepack/receivepack_test.go delete mode 100644 go/internal/command/shared/accessverifier/accessverifier.go delete mode 100644 go/internal/command/shared/accessverifier/accessverifier_test.go delete mode 100644 go/internal/command/shared/disallowedcommand/disallowedcommand.go delete mode 100644 go/internal/command/twofactorrecover/twofactorrecover.go delete mode 100644 go/internal/command/twofactorrecover/twofactorrecover_test.go delete mode 100644 go/internal/command/uploadarchive/gitalycall.go delete mode 100644 go/internal/command/uploadarchive/gitalycall_test.go delete mode 100644 go/internal/command/uploadarchive/uploadarchive.go delete mode 100644 go/internal/command/uploadarchive/uploadarchive_test.go delete mode 100644 go/internal/command/uploadpack/gitalycall.go delete mode 100644 go/internal/command/uploadpack/gitalycall_test.go delete mode 100644 go/internal/command/uploadpack/uploadpack.go delete mode 100644 go/internal/command/uploadpack/uploadpack_test.go delete mode 100644 go/internal/config/config.go delete mode 100644 go/internal/config/config_test.go delete mode 100644 go/internal/config/httpclient.go delete mode 100644 go/internal/config/httpclient_test.go delete mode 100644 go/internal/executable/executable.go delete mode 100644 go/internal/executable/executable_test.go delete mode 100644 go/internal/gitlabnet/accessverifier/client.go delete mode 100644 go/internal/gitlabnet/accessverifier/client_test.go delete mode 100644 go/internal/gitlabnet/authorizedkeys/client.go delete mode 100644 go/internal/gitlabnet/authorizedkeys/client_test.go delete mode 100644 go/internal/gitlabnet/client.go delete mode 100644 go/internal/gitlabnet/client_test.go delete mode 100644 go/internal/gitlabnet/discover/client.go delete mode 100644 go/internal/gitlabnet/discover/client_test.go delete mode 100644 go/internal/gitlabnet/healthcheck/client.go delete mode 100644 go/internal/gitlabnet/healthcheck/client_test.go delete mode 100644 go/internal/gitlabnet/httpclient_test.go delete mode 100644 go/internal/gitlabnet/httpsclient_test.go delete mode 100644 go/internal/gitlabnet/lfsauthenticate/client.go delete mode 100644 go/internal/gitlabnet/lfsauthenticate/client_test.go delete mode 100644 go/internal/gitlabnet/testserver/gitalyserver.go delete mode 100644 go/internal/gitlabnet/testserver/testserver.go delete mode 100644 go/internal/gitlabnet/twofactorrecover/client.go delete mode 100644 go/internal/gitlabnet/twofactorrecover/client_test.go delete mode 100644 go/internal/handler/exec.go delete mode 100644 go/internal/handler/exec_test.go delete mode 100644 go/internal/keyline/key_line.go delete mode 100644 go/internal/keyline/key_line_test.go delete mode 100644 go/internal/logger/logger.go delete mode 100644 go/internal/sshenv/sshenv.go delete mode 100644 go/internal/sshenv/sshenv_test.go delete mode 100644 go/internal/testhelper/requesthandlers/requesthandlers.go delete mode 100644 go/internal/testhelper/testdata/testroot/.gitlab_shell_secret delete mode 100644 go/internal/testhelper/testdata/testroot/certs/invalid/server.crt delete mode 100644 go/internal/testhelper/testdata/testroot/certs/valid/dir/.gitkeep delete mode 100644 go/internal/testhelper/testdata/testroot/certs/valid/server.crt delete mode 100644 go/internal/testhelper/testdata/testroot/certs/valid/server.key delete mode 100644 go/internal/testhelper/testdata/testroot/config.yml delete mode 100644 go/internal/testhelper/testdata/testroot/custom/my-contents-is-secret delete mode 100644 go/internal/testhelper/testdata/testroot/gitlab-shell.log delete mode 100644 go/internal/testhelper/testdata/testroot/responses/allowed.json delete mode 100644 go/internal/testhelper/testdata/testroot/responses/allowed_with_payload.json delete mode 100644 go/internal/testhelper/testhelper.go create mode 100644 internal/command/authorizedkeys/authorized_keys.go create mode 100644 internal/command/authorizedkeys/authorized_keys_test.go create mode 100644 internal/command/authorizedprincipals/authorized_principals.go create mode 100644 internal/command/authorizedprincipals/authorized_principals_test.go create mode 100644 internal/command/command.go create mode 100644 internal/command/command_test.go create mode 100644 internal/command/commandargs/authorized_keys.go create mode 100644 internal/command/commandargs/authorized_principals.go create mode 100644 internal/command/commandargs/command_args.go create mode 100644 internal/command/commandargs/command_args_test.go create mode 100644 internal/command/commandargs/generic_args.go create mode 100644 internal/command/commandargs/shell.go create mode 100644 internal/command/discover/discover.go create mode 100644 internal/command/discover/discover_test.go create mode 100644 internal/command/healthcheck/healthcheck.go create mode 100644 internal/command/healthcheck/healthcheck_test.go create mode 100644 internal/command/lfsauthenticate/lfsauthenticate.go create mode 100644 internal/command/lfsauthenticate/lfsauthenticate_test.go create mode 100644 internal/command/readwriter/readwriter.go create mode 100644 internal/command/receivepack/customaction.go create mode 100644 internal/command/receivepack/customaction_test.go create mode 100644 internal/command/receivepack/gitalycall.go create mode 100644 internal/command/receivepack/gitalycall_test.go create mode 100644 internal/command/receivepack/receivepack.go create mode 100644 internal/command/receivepack/receivepack_test.go create mode 100644 internal/command/shared/accessverifier/accessverifier.go create mode 100644 internal/command/shared/accessverifier/accessverifier_test.go create mode 100644 internal/command/shared/disallowedcommand/disallowedcommand.go create mode 100644 internal/command/twofactorrecover/twofactorrecover.go create mode 100644 internal/command/twofactorrecover/twofactorrecover_test.go create mode 100644 internal/command/uploadarchive/gitalycall.go create mode 100644 internal/command/uploadarchive/gitalycall_test.go create mode 100644 internal/command/uploadarchive/uploadarchive.go create mode 100644 internal/command/uploadarchive/uploadarchive_test.go create mode 100644 internal/command/uploadpack/gitalycall.go create mode 100644 internal/command/uploadpack/gitalycall_test.go create mode 100644 internal/command/uploadpack/uploadpack.go create mode 100644 internal/command/uploadpack/uploadpack_test.go create mode 100644 internal/config/config.go create mode 100644 internal/config/config_test.go create mode 100644 internal/config/httpclient.go create mode 100644 internal/config/httpclient_test.go create mode 100644 internal/executable/executable.go create mode 100644 internal/executable/executable_test.go create mode 100644 internal/gitlabnet/accessverifier/client.go create mode 100644 internal/gitlabnet/accessverifier/client_test.go create mode 100644 internal/gitlabnet/authorizedkeys/client.go create mode 100644 internal/gitlabnet/authorizedkeys/client_test.go create mode 100644 internal/gitlabnet/client.go create mode 100644 internal/gitlabnet/client_test.go create mode 100644 internal/gitlabnet/discover/client.go create mode 100644 internal/gitlabnet/discover/client_test.go create mode 100644 internal/gitlabnet/healthcheck/client.go create mode 100644 internal/gitlabnet/healthcheck/client_test.go create mode 100644 internal/gitlabnet/httpclient_test.go create mode 100644 internal/gitlabnet/httpsclient_test.go create mode 100644 internal/gitlabnet/lfsauthenticate/client.go create mode 100644 internal/gitlabnet/lfsauthenticate/client_test.go create mode 100644 internal/gitlabnet/testserver/gitalyserver.go create mode 100644 internal/gitlabnet/testserver/testserver.go create mode 100644 internal/gitlabnet/twofactorrecover/client.go create mode 100644 internal/gitlabnet/twofactorrecover/client_test.go create mode 100644 internal/handler/exec.go create mode 100644 internal/handler/exec_test.go create mode 100644 internal/keyline/key_line.go create mode 100644 internal/keyline/key_line_test.go create mode 100644 internal/logger/logger.go create mode 100644 internal/sshenv/sshenv.go create mode 100644 internal/sshenv/sshenv_test.go create mode 100644 internal/testhelper/requesthandlers/requesthandlers.go create mode 100644 internal/testhelper/testdata/testroot/.gitlab_shell_secret create mode 100644 internal/testhelper/testdata/testroot/certs/invalid/server.crt create mode 100644 internal/testhelper/testdata/testroot/certs/valid/dir/.gitkeep create mode 100644 internal/testhelper/testdata/testroot/certs/valid/server.crt create mode 100644 internal/testhelper/testdata/testroot/certs/valid/server.key create mode 100644 internal/testhelper/testdata/testroot/config.yml create mode 100644 internal/testhelper/testdata/testroot/custom/my-contents-is-secret create mode 100644 internal/testhelper/testdata/testroot/gitlab-shell.log create mode 100644 internal/testhelper/testdata/testroot/responses/allowed.json create mode 100644 internal/testhelper/testdata/testroot/responses/allowed_with_payload.json create mode 100644 internal/testhelper/testhelper.go diff --git a/cmd/check/main.go b/cmd/check/main.go new file mode 100644 index 0000000..1d32f77 --- /dev/null +++ b/cmd/check/main.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + "os" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command" + "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/executable" +) + +func main() { + readWriter := &readwriter.ReadWriter{ + Out: os.Stdout, + In: os.Stdin, + ErrOut: os.Stderr, + } + + executable, err := executable.New(executable.Healthcheck) + if err != nil { + fmt.Fprintln(readWriter.ErrOut, "Failed to determine executable, exiting") + os.Exit(1) + } + + config, err := config.NewFromDir(executable.RootDir) + if err != nil { + fmt.Fprintln(readWriter.ErrOut, "Failed to read config, exiting") + os.Exit(1) + } + + cmd, err := command.New(executable, os.Args[1:], config, readWriter) + if err != nil { + fmt.Fprintf(readWriter.ErrOut, "%v\n", err) + os.Exit(1) + } + + if err = cmd.Execute(); err != nil { + fmt.Fprintf(readWriter.ErrOut, "%v\n", err) + os.Exit(1) + } +} diff --git a/cmd/gitlab-shell-authorized-keys-check/main.go b/cmd/gitlab-shell-authorized-keys-check/main.go new file mode 100644 index 0000000..d8bb524 --- /dev/null +++ b/cmd/gitlab-shell-authorized-keys-check/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "os" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command" + "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/executable" +) + +func main() { + readWriter := &readwriter.ReadWriter{ + Out: os.Stdout, + In: os.Stdin, + ErrOut: os.Stderr, + } + + executable, err := executable.New(executable.AuthorizedKeysCheck) + if err != nil { + fmt.Fprintln(readWriter.ErrOut, "Failed to determine executable, exiting") + os.Exit(1) + } + + config, err := config.NewFromDir(executable.RootDir) + if err != nil { + fmt.Fprintln(readWriter.ErrOut, "Failed to read config, exiting") + os.Exit(1) + } + + cmd, err := command.New(executable, os.Args[1:], config, readWriter) + if err != nil { + // For now this could happen if `SSH_CONNECTION` is not set on + // the environment + fmt.Fprintf(readWriter.ErrOut, "%v\n", err) + os.Exit(1) + } + + if err = cmd.Execute(); err != nil { + fmt.Fprintf(readWriter.ErrOut, "%v\n", err) + os.Exit(1) + } +} diff --git a/cmd/gitlab-shell-authorized-principals-check/main.go b/cmd/gitlab-shell-authorized-principals-check/main.go new file mode 100644 index 0000000..5c9caf8 --- /dev/null +++ b/cmd/gitlab-shell-authorized-principals-check/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "os" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command" + "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/executable" +) + +func main() { + readWriter := &readwriter.ReadWriter{ + Out: os.Stdout, + In: os.Stdin, + ErrOut: os.Stderr, + } + + executable, err := executable.New(executable.AuthorizedPrincipalsCheck) + if err != nil { + fmt.Fprintln(readWriter.ErrOut, "Failed to determine executable, exiting") + os.Exit(1) + } + + config, err := config.NewFromDir(executable.RootDir) + if err != nil { + fmt.Fprintln(readWriter.ErrOut, "Failed to read config, exiting") + os.Exit(1) + } + + cmd, err := command.New(executable, os.Args[1:], config, readWriter) + if err != nil { + // For now this could happen if `SSH_CONNECTION` is not set on + // the environment + fmt.Fprintf(readWriter.ErrOut, "%v\n", err) + os.Exit(1) + } + + if err = cmd.Execute(); err != nil { + fmt.Fprintf(readWriter.ErrOut, "%v\n", err) + os.Exit(1) + } +} diff --git a/cmd/gitlab-shell/main.go b/cmd/gitlab-shell/main.go new file mode 100644 index 0000000..adb5198 --- /dev/null +++ b/cmd/gitlab-shell/main.go @@ -0,0 +1,44 @@ +package main + +import ( + "fmt" + "os" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command" + "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/executable" +) + +func main() { + readWriter := &readwriter.ReadWriter{ + Out: os.Stdout, + In: os.Stdin, + ErrOut: os.Stderr, + } + + executable, err := executable.New(executable.GitlabShell) + if err != nil { + fmt.Fprintln(readWriter.ErrOut, "Failed to determine executable, exiting") + os.Exit(1) + } + + config, err := config.NewFromDir(executable.RootDir) + if err != nil { + fmt.Fprintln(readWriter.ErrOut, "Failed to read config, exiting") + os.Exit(1) + } + + cmd, err := command.New(executable, os.Args[1:], config, readWriter) + if err != nil { + // For now this could happen if `SSH_CONNECTION` is not set on + // the environment + fmt.Fprintf(readWriter.ErrOut, "%v\n", err) + os.Exit(1) + } + + if err = cmd.Execute(); err != nil { + fmt.Fprintf(readWriter.ErrOut, "%v\n", err) + os.Exit(1) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..1083d3a --- /dev/null +++ b/go.mod @@ -0,0 +1,16 @@ +module gitlab.com/gitlab-org/gitlab-shell/go + +go 1.12 + +require ( + github.com/mattn/go-shellwords v0.0.0-20190425161501-2444a32a19f4 + github.com/otiai10/copy v1.0.1 + github.com/otiai10/curr v0.0.0-20190513014714-f5a3d24e5776 // indirect + github.com/sirupsen/logrus v1.2.0 + github.com/stretchr/testify v1.3.0 + gitlab.com/gitlab-org/gitaly v1.68.0 + gitlab.com/gitlab-org/labkit v0.0.0-20190221122536-0c3fc7cdd57c + google.golang.org/grpc v1.24.0 + gopkg.in/DataDog/dd-trace-go.v1 v1.9.0 // indirect + gopkg.in/yaml.v2 v2.2.2 +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..7da27e7 --- /dev/null +++ b/go.sum @@ -0,0 +1,184 @@ +bou.ke/monkey v1.0.1 h1:zEMLInw9xvNakzUUPjfS4Ds6jYPqCFx3m7bRmG5NH2U= +bou.ke/monkey v1.0.1/go.mod h1:FgHuK96Rv2Nlf+0u1OOVDpCMdsWyOFmeeketDHE7LIg= +cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ= +cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= +github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= +github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= +github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= +github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= +github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= +github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= +github.com/certifi/gocertifi v0.0.0-20180905225744-ee1a9a0726d2 h1:MmeatFT1pTPSVb4nkPmBFN/LRZ97vPjsFKsZrU3KKTs= +github.com/certifi/gocertifi v0.0.0-20180905225744-ee1a9a0726d2/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= +github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cloudflare/tableflip v0.0.0-20190329062924-8392f1641731/go.mod h1:erh4dYezoMVbIa52pi7i1Du7+cXOgqNuTamt10qvMoA= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/getsentry/raven-go v0.1.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/getsentry/raven-go v0.1.2 h1:4V0z512S5mZXiBvmW2RbuZBSIY1sEdMNsPjpx2zwtSE= +github.com/getsentry/raven-go v0.1.2/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= +github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= +github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= +github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= +github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= +github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= +github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= +github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= +github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= +github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM= +github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= +github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= +github.com/libgit2/git2go v0.0.0-20190104134018-ecaeb7a21d47/go.mod h1:4bKN42efkbNYMZlvDfxGDxzl066GhpvIircZDsm8Y+Y= +github.com/lightstep/lightstep-tracer-go v0.15.6 h1:D0GGa7afJ7GcQvu5as6ssLEEKYXvRgKI5d5cevtz8r4= +github.com/lightstep/lightstep-tracer-go v0.15.6/go.mod h1:6AMpwZpsyCFwSovxzM78e+AsYxE8sGwiM6C3TytaWeI= +github.com/mattn/go-shellwords v0.0.0-20190425161501-2444a32a19f4 h1:w5NBKXwiBRfrigVdGUCi0mftyNH7U6PxIhCWCG4UOPw= +github.com/mattn/go-shellwords v0.0.0-20190425161501-2444a32a19f4/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= +github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= +github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/otiai10/copy v1.0.1 h1:gtBjD8aq4nychvRZ2CyJvFWAw0aja+VHazDdruZKGZA= +github.com/otiai10/copy v1.0.1/go.mod h1:8bMCJrAqOtN/d9oyh5HR7HhLQMvcGMpGdwRDYsfOCHc= +github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= +github.com/otiai10/curr v0.0.0-20190513014714-f5a3d24e5776 h1:o59bHXu8Ejas8Kq6pjoVJQ9/neN66SM8AKh6wI42BBs= +github.com/otiai10/curr v0.0.0-20190513014714-f5a3d24e5776/go.mod h1:3HNVkVOU7vZeFXocWuvtcS0XSFLcf2XUSDHkq9t1jU4= +github.com/otiai10/mint v1.2.3 h1:PsrRBmrxR68kyNu6YlqYHbNlItc5vOkuS6LBEsNttVA= +github.com/otiai10/mint v1.2.3/go.mod h1:YnfyPNhBvnY8bW4SGQHCs/aAFhkgySlMZbrF5U0bOVw= +github.com/otiai10/mint v1.2.4 h1:DxYL0itZyPaR5Z9HILdxSoHx+gNs6Yx+neOGS3IVUk0= +github.com/otiai10/mint v1.2.4/go.mod h1:d+b7n/0R3tdyUYYylALXpWQ/kTN+QobSq/4SRGBkR3M= +github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ= +github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= +github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= +github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= +github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= +github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= +github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= +github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/tinylib/msgp v1.0.2 h1:DfdQrzQa7Yh2es9SuLkixqxuXS2SxsdYn0KbdrOGWD8= +github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU= +github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= +github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo= +github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= +github.com/uber/jaeger-client-go v2.15.0+incompatible h1:NP3qsSqNxh8VYr956ur1N/1C1PjvOJnJykCzcD5QHbk= +github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= +github.com/uber/jaeger-lib v1.5.0 h1:OHbgr8l656Ub3Fw5k9SWnBfIEwvoHQ+W2y+Aa9D1Uyo= +github.com/uber/jaeger-lib v1.5.0/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= +gitlab.com/gitlab-org/gitaly v1.68.0 h1:VlcJs1+PrhW7lqJUU7Fh1q8FMJujmbbivdfde/cwB98= +gitlab.com/gitlab-org/gitaly v1.68.0/go.mod h1:/pCsB918Zu5wFchZ9hLYin9WkJ2yQqdVNz0zlv5HbXg= +gitlab.com/gitlab-org/labkit v0.0.0-20190221122536-0c3fc7cdd57c h1:xo48LcGsTCasKcJpQDBCCuZU+aP8uGaboUVvD7Lgm6g= +gitlab.com/gitlab-org/labkit v0.0.0-20190221122536-0c3fc7cdd57c/go.mod h1:rYhLgfrbEcyfinG+R3EvKu6bZSsmwQqcXzLfHWSfUKM= +go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc= +golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= +golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181106065722-10aee1819953 h1:LuZIitY8waaxUfNIdtajyE/YzA/zyf0YxXG27VpLrkg= +golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= +golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= +golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= +golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= +google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898 h1:yvw+zsSmSM02Z5H3ZdEV7B7Ql7eFrjQTnmByJvK+3J8= +google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= +google.golang.org/grpc v1.16.0 h1:dz5IJGuC2BB7qXR5AyHNwAUBhZscK2xVez7mznh72sY= +google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= +google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s= +google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= +gopkg.in/DataDog/dd-trace-go.v1 v1.7.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg= +gopkg.in/DataDog/dd-trace-go.v1 v1.9.0 h1:lV7bxZqgk8AxhwWV+biDJCFR745Os6Trg+/dEs/D9WA= +gopkg.in/DataDog/dd-trace-go.v1 v1.9.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg= +gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= +honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/go/README.md b/go/README.md deleted file mode 100644 index 6bbc03e..0000000 --- a/go/README.md +++ /dev/null @@ -1,18 +0,0 @@ -# Go executables for gitlab-shell - -This directory contains Go executables for use in gitlab-shell. To add -a new command `foobar` create a subdirectory `cmd/foobar` and put your -code in `package main` under `cmd/foobar`. This will automatically get -compiled into `bin/foobar` by `../bin/compile`. - -## Vendoring - -We use vendoring in order to include third-party Go libraries. This -project uses [govendor](https://github.com/kardianos/govendor). - -To update e.g. `gitaly-proto` run the following command in the root -directory of the project. - -``` -support/go-update-vendor gitlab.com/gitlab-org/gitaly-proto/go@v0.109.0 -``` diff --git a/go/cmd/check/main.go b/go/cmd/check/main.go deleted file mode 100644 index 1d32f77..0000000 --- a/go/cmd/check/main.go +++ /dev/null @@ -1,42 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/command" - "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/executable" -) - -func main() { - readWriter := &readwriter.ReadWriter{ - Out: os.Stdout, - In: os.Stdin, - ErrOut: os.Stderr, - } - - executable, err := executable.New(executable.Healthcheck) - if err != nil { - fmt.Fprintln(readWriter.ErrOut, "Failed to determine executable, exiting") - os.Exit(1) - } - - config, err := config.NewFromDir(executable.RootDir) - if err != nil { - fmt.Fprintln(readWriter.ErrOut, "Failed to read config, exiting") - os.Exit(1) - } - - cmd, err := command.New(executable, os.Args[1:], config, readWriter) - if err != nil { - fmt.Fprintf(readWriter.ErrOut, "%v\n", err) - os.Exit(1) - } - - if err = cmd.Execute(); err != nil { - fmt.Fprintf(readWriter.ErrOut, "%v\n", err) - os.Exit(1) - } -} diff --git a/go/cmd/gitlab-shell-authorized-keys-check/main.go b/go/cmd/gitlab-shell-authorized-keys-check/main.go deleted file mode 100644 index d8bb524..0000000 --- a/go/cmd/gitlab-shell-authorized-keys-check/main.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/command" - "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/executable" -) - -func main() { - readWriter := &readwriter.ReadWriter{ - Out: os.Stdout, - In: os.Stdin, - ErrOut: os.Stderr, - } - - executable, err := executable.New(executable.AuthorizedKeysCheck) - if err != nil { - fmt.Fprintln(readWriter.ErrOut, "Failed to determine executable, exiting") - os.Exit(1) - } - - config, err := config.NewFromDir(executable.RootDir) - if err != nil { - fmt.Fprintln(readWriter.ErrOut, "Failed to read config, exiting") - os.Exit(1) - } - - cmd, err := command.New(executable, os.Args[1:], config, readWriter) - if err != nil { - // For now this could happen if `SSH_CONNECTION` is not set on - // the environment - fmt.Fprintf(readWriter.ErrOut, "%v\n", err) - os.Exit(1) - } - - if err = cmd.Execute(); err != nil { - fmt.Fprintf(readWriter.ErrOut, "%v\n", err) - os.Exit(1) - } -} diff --git a/go/cmd/gitlab-shell-authorized-principals-check/main.go b/go/cmd/gitlab-shell-authorized-principals-check/main.go deleted file mode 100644 index 5c9caf8..0000000 --- a/go/cmd/gitlab-shell-authorized-principals-check/main.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/command" - "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/executable" -) - -func main() { - readWriter := &readwriter.ReadWriter{ - Out: os.Stdout, - In: os.Stdin, - ErrOut: os.Stderr, - } - - executable, err := executable.New(executable.AuthorizedPrincipalsCheck) - if err != nil { - fmt.Fprintln(readWriter.ErrOut, "Failed to determine executable, exiting") - os.Exit(1) - } - - config, err := config.NewFromDir(executable.RootDir) - if err != nil { - fmt.Fprintln(readWriter.ErrOut, "Failed to read config, exiting") - os.Exit(1) - } - - cmd, err := command.New(executable, os.Args[1:], config, readWriter) - if err != nil { - // For now this could happen if `SSH_CONNECTION` is not set on - // the environment - fmt.Fprintf(readWriter.ErrOut, "%v\n", err) - os.Exit(1) - } - - if err = cmd.Execute(); err != nil { - fmt.Fprintf(readWriter.ErrOut, "%v\n", err) - os.Exit(1) - } -} diff --git a/go/cmd/gitlab-shell/main.go b/go/cmd/gitlab-shell/main.go deleted file mode 100644 index adb5198..0000000 --- a/go/cmd/gitlab-shell/main.go +++ /dev/null @@ -1,44 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/command" - "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/executable" -) - -func main() { - readWriter := &readwriter.ReadWriter{ - Out: os.Stdout, - In: os.Stdin, - ErrOut: os.Stderr, - } - - executable, err := executable.New(executable.GitlabShell) - if err != nil { - fmt.Fprintln(readWriter.ErrOut, "Failed to determine executable, exiting") - os.Exit(1) - } - - config, err := config.NewFromDir(executable.RootDir) - if err != nil { - fmt.Fprintln(readWriter.ErrOut, "Failed to read config, exiting") - os.Exit(1) - } - - cmd, err := command.New(executable, os.Args[1:], config, readWriter) - if err != nil { - // For now this could happen if `SSH_CONNECTION` is not set on - // the environment - fmt.Fprintf(readWriter.ErrOut, "%v\n", err) - os.Exit(1) - } - - if err = cmd.Execute(); err != nil { - fmt.Fprintf(readWriter.ErrOut, "%v\n", err) - os.Exit(1) - } -} diff --git a/go/go.mod b/go/go.mod deleted file mode 100644 index 1083d3a..0000000 --- a/go/go.mod +++ /dev/null @@ -1,16 +0,0 @@ -module gitlab.com/gitlab-org/gitlab-shell/go - -go 1.12 - -require ( - github.com/mattn/go-shellwords v0.0.0-20190425161501-2444a32a19f4 - github.com/otiai10/copy v1.0.1 - github.com/otiai10/curr v0.0.0-20190513014714-f5a3d24e5776 // indirect - github.com/sirupsen/logrus v1.2.0 - github.com/stretchr/testify v1.3.0 - gitlab.com/gitlab-org/gitaly v1.68.0 - gitlab.com/gitlab-org/labkit v0.0.0-20190221122536-0c3fc7cdd57c - google.golang.org/grpc v1.24.0 - gopkg.in/DataDog/dd-trace-go.v1 v1.9.0 // indirect - gopkg.in/yaml.v2 v2.2.2 -) diff --git a/go/go.sum b/go/go.sum deleted file mode 100644 index 7da27e7..0000000 --- a/go/go.sum +++ /dev/null @@ -1,184 +0,0 @@ -bou.ke/monkey v1.0.1 h1:zEMLInw9xvNakzUUPjfS4Ds6jYPqCFx3m7bRmG5NH2U= -bou.ke/monkey v1.0.1/go.mod h1:FgHuK96Rv2Nlf+0u1OOVDpCMdsWyOFmeeketDHE7LIg= -cloud.google.com/go v0.26.0 h1:e0WKqKTd5BnrG8aKH3J3h+QvEIQtSUcf2n5UZ5ZgLtQ= -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -github.com/BurntSushi/toml v0.3.1 h1:WXkYYl6Yr3qBf1K79EBnL4mak0OimBfB0XUf9Vl28OQ= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= -github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= -github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= -github.com/beorn7/perks v1.0.0 h1:HWo1m869IqiPhD389kmkxeTalrjNbbJTC8LXupb+sl0= -github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= -github.com/certifi/gocertifi v0.0.0-20180905225744-ee1a9a0726d2 h1:MmeatFT1pTPSVb4nkPmBFN/LRZ97vPjsFKsZrU3KKTs= -github.com/certifi/gocertifi v0.0.0-20180905225744-ee1a9a0726d2/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/tableflip v0.0.0-20190329062924-8392f1641731/go.mod h1:erh4dYezoMVbIa52pi7i1Du7+cXOgqNuTamt10qvMoA= -github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd h1:qMd81Ts1T2OTKmB4acZcyKaMtRnY5Y44NuXGX2GFJ1w= -github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= -github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= -github.com/getsentry/raven-go v0.1.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= -github.com/getsentry/raven-go v0.1.2 h1:4V0z512S5mZXiBvmW2RbuZBSIY1sEdMNsPjpx2zwtSE= -github.com/getsentry/raven-go v0.1.2/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= -github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= -github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= -github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= -github.com/gogo/protobuf v1.1.1 h1:72R+M5VuhED/KujmZVcIquuo8mBgX4oVda//DQb3PXo= -github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b h1:VKtxabqXZkF25pY9ekfRL6a582T4P37/31XEstQ5p58= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2 h1:6nsPYzhq5kReh6QImI3k5qWzO4PEbvbIW2cwSfR/6xs= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY= -github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0 h1:Iju5GlWwrvL6UBg4zJJt3btmonfrMlCDdsejg4CZE7c= -github.com/grpc-ecosystem/go-grpc-middleware v1.0.0/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= -github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= -github.com/hpcloud/tail v1.0.0 h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI= -github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= -github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= -github.com/kelseyhightower/envconfig v1.3.0 h1:IvRS4f2VcIQy6j4ORGIf9145T/AsUB+oY8LyvN8BXNM= -github.com/kelseyhightower/envconfig v1.3.0/go.mod h1:cccZRl6mQpaq41TPp5QxidR+Sa3axMbJDNb//FQX6Gg= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/konsorten/go-windows-terminal-sequences v1.0.1 h1:mweAR1A6xJ3oS2pRaGiHgQ4OO8tzTaLawm8vnODuwDk= -github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= -github.com/libgit2/git2go v0.0.0-20190104134018-ecaeb7a21d47/go.mod h1:4bKN42efkbNYMZlvDfxGDxzl066GhpvIircZDsm8Y+Y= -github.com/lightstep/lightstep-tracer-go v0.15.6 h1:D0GGa7afJ7GcQvu5as6ssLEEKYXvRgKI5d5cevtz8r4= -github.com/lightstep/lightstep-tracer-go v0.15.6/go.mod h1:6AMpwZpsyCFwSovxzM78e+AsYxE8sGwiM6C3TytaWeI= -github.com/mattn/go-shellwords v0.0.0-20190425161501-2444a32a19f4 h1:w5NBKXwiBRfrigVdGUCi0mftyNH7U6PxIhCWCG4UOPw= -github.com/mattn/go-shellwords v0.0.0-20190425161501-2444a32a19f4/go.mod h1:3xCvwCdWdlDJUrvuMn7Wuy9eWs4pE8vqg+NOMyg4B2o= -github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= -github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= -github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/ginkgo v1.7.0 h1:WSHQ+IS43OoUrWtD1/bbclrwK8TTH5hzp+umCiuxHgs= -github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= -github.com/onsi/gomega v1.4.3 h1:RE1xgDvH7imwFD45h+u2SgIfERHlS2yNG4DObb5BSKU= -github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= -github.com/opentracing/opentracing-go v1.0.2 h1:3jA2P6O1F9UOrWVpwrIo17pu01KWvNWg4X946/Y5Zwg= -github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= -github.com/otiai10/copy v1.0.1 h1:gtBjD8aq4nychvRZ2CyJvFWAw0aja+VHazDdruZKGZA= -github.com/otiai10/copy v1.0.1/go.mod h1:8bMCJrAqOtN/d9oyh5HR7HhLQMvcGMpGdwRDYsfOCHc= -github.com/otiai10/curr v0.0.0-20150429015615-9b4961190c95/go.mod h1:9qAhocn7zKJG+0mI8eUu6xqkFDYS2kb2saOteoSB3cE= -github.com/otiai10/curr v0.0.0-20190513014714-f5a3d24e5776 h1:o59bHXu8Ejas8Kq6pjoVJQ9/neN66SM8AKh6wI42BBs= -github.com/otiai10/curr v0.0.0-20190513014714-f5a3d24e5776/go.mod h1:3HNVkVOU7vZeFXocWuvtcS0XSFLcf2XUSDHkq9t1jU4= -github.com/otiai10/mint v1.2.3 h1:PsrRBmrxR68kyNu6YlqYHbNlItc5vOkuS6LBEsNttVA= -github.com/otiai10/mint v1.2.3/go.mod h1:YnfyPNhBvnY8bW4SGQHCs/aAFhkgySlMZbrF5U0bOVw= -github.com/otiai10/mint v1.2.4 h1:DxYL0itZyPaR5Z9HILdxSoHx+gNs6Yx+neOGS3IVUk0= -github.com/otiai10/mint v1.2.4/go.mod h1:d+b7n/0R3tdyUYYylALXpWQ/kTN+QobSq/4SRGBkR3M= -github.com/philhofer/fwd v1.0.0 h1:UbZqGr5Y38ApvM/V/jEljVxwocdweyH+vmYvRPBnbqQ= -github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= -github.com/pkg/errors v0.8.0 h1:WdK/asTD0HN+q6hsWO3/vpuAkAr+tw6aNJNDFFf0+qw= -github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= -github.com/prometheus/client_golang v1.0.0 h1:vrDKnkGzuGvhNAL56c7DBz29ZL+KxnoR0x7enabFceM= -github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= -github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90 h1:S/YWwWx/RA8rT8tKFRuGUZhuA90OyIBpPCXkcbwU8DE= -github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/common v0.4.1 h1:K0MGApIoQvMw27RTdJkPbr3JZ7DNbtxQNyi5STVM6Kw= -github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= -github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= -github.com/prometheus/procfs v0.0.2 h1:6LJUbpNm42llc4HRCuvApCSWB/WfhuNo9K98Q9sNGfs= -github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= -github.com/sirupsen/logrus v1.2.0 h1:juTguoYk5qI21pwyTXY3B3Y5cOTH3ZUyZCg1v/mihuo= -github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1w= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.3.0 h1:TivCn/peBQ7UY8ooIcPgZFpTNSz0Q2U6UrFlUfqbe0Q= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/tinylib/msgp v1.0.2 h1:DfdQrzQa7Yh2es9SuLkixqxuXS2SxsdYn0KbdrOGWD8= -github.com/tinylib/msgp v1.0.2/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= -github.com/tinylib/msgp v1.1.0 h1:9fQd+ICuRIu/ue4vxJZu6/LzxN0HwMds2nq/0cFvxHU= -github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE= -github.com/uber-go/atomic v1.3.2 h1:Azu9lPBWRNKzYXSIwRfgRuDuS0YKsK4NFhiQv98gkxo= -github.com/uber-go/atomic v1.3.2/go.mod h1:/Ct5t2lcmbJ4OSe/waGBoaVvVqtO0bmtfVNex1PFV8g= -github.com/uber/jaeger-client-go v2.15.0+incompatible h1:NP3qsSqNxh8VYr956ur1N/1C1PjvOJnJykCzcD5QHbk= -github.com/uber/jaeger-client-go v2.15.0+incompatible/go.mod h1:WVhlPFC8FDjOFMMWRy2pZqQJSXxYSwNYOkTr/Z6d3Kk= -github.com/uber/jaeger-lib v1.5.0 h1:OHbgr8l656Ub3Fw5k9SWnBfIEwvoHQ+W2y+Aa9D1Uyo= -github.com/uber/jaeger-lib v1.5.0/go.mod h1:ComeNDZlWwrWnDv8aPp0Ba6+uUTzImX/AauajbLI56U= -gitlab.com/gitlab-org/gitaly v1.68.0 h1:VlcJs1+PrhW7lqJUU7Fh1q8FMJujmbbivdfde/cwB98= -gitlab.com/gitlab-org/gitaly v1.68.0/go.mod h1:/pCsB918Zu5wFchZ9hLYin9WkJ2yQqdVNz0zlv5HbXg= -gitlab.com/gitlab-org/labkit v0.0.0-20190221122536-0c3fc7cdd57c h1:xo48LcGsTCasKcJpQDBCCuZU+aP8uGaboUVvD7Lgm6g= -gitlab.com/gitlab-org/labkit v0.0.0-20190221122536-0c3fc7cdd57c/go.mod h1:rYhLgfrbEcyfinG+R3EvKu6bZSsmwQqcXzLfHWSfUKM= -go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5 h1:8dUaAV7K4uHsF56JQWkprecIQKdPHtR9jCHF5nB8uzc= -golang.org/x/crypto v0.0.0-20190530122614-20be4c3c3ed5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181106065722-10aee1819953 h1:LuZIitY8waaxUfNIdtajyE/YzA/zyf0YxXG27VpLrkg= -golang.org/x/net v0.0.0-20181106065722-10aee1819953/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980 h1:dfGZHvZk057jK2MCeWus/TowKpJ8y4AmooUzdBSR9GU= -golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be h1:vEDujvNQGv4jgYKudGeI/+DAX4Jffq6hpD55MmoEvKs= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58 h1:8gQV6CLnAEikrhgkHFbMAEhagSSnXWGV915qUMm9mrU= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894 h1:Cz4ceDQGXuKRnVBDTS23GTn/pU5OE2C0WrNTOYK1Uuc= -golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -google.golang.org/appengine v1.1.0 h1:igQkv0AAhEIvTEpD5LIpAfav2eeVO9HBTjvKHVJPRSs= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898 h1:yvw+zsSmSM02Z5H3ZdEV7B7Ql7eFrjQTnmByJvK+3J8= -google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= -google.golang.org/grpc v1.16.0 h1:dz5IJGuC2BB7qXR5AyHNwAUBhZscK2xVez7mznh72sY= -google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= -google.golang.org/grpc v1.24.0 h1:vb/1TCsVn3DcJlQ0Gs1yB1pKI6Do2/QNwxdKqmc/b0s= -google.golang.org/grpc v1.24.0/go.mod h1:XDChyiUovWa60DnaeDeZmSW86xtLtjtZbwvSiRnRtcA= -gopkg.in/DataDog/dd-trace-go.v1 v1.7.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg= -gopkg.in/DataDog/dd-trace-go.v1 v1.9.0 h1:lV7bxZqgk8AxhwWV+biDJCFR745Os6Trg+/dEs/D9WA= -gopkg.in/DataDog/dd-trace-go.v1 v1.9.0/go.mod h1:DVp8HmDh8PuTu2Z0fVVlBsyWaC++fzwVCaGWylTe3tg= -gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= -gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE= -gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/go/internal/command/authorizedkeys/authorized_keys.go b/go/internal/command/authorizedkeys/authorized_keys.go deleted file mode 100644 index d5837b0..0000000 --- a/go/internal/command/authorizedkeys/authorized_keys.go +++ /dev/null @@ -1,61 +0,0 @@ -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/go/internal/command/authorizedkeys/authorized_keys_test.go b/go/internal/command/authorizedkeys/authorized_keys_test.go deleted file mode 100644 index 5cde366..0000000 --- a/go/internal/command/authorizedkeys/authorized_keys_test.go +++ /dev/null @@ -1,90 +0,0 @@ -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/go/internal/command/authorizedprincipals/authorized_principals.go b/go/internal/command/authorizedprincipals/authorized_principals.go deleted file mode 100644 index b04e5a4..0000000 --- a/go/internal/command/authorizedprincipals/authorized_principals.go +++ /dev/null @@ -1,47 +0,0 @@ -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/go/internal/command/authorizedprincipals/authorized_principals_test.go b/go/internal/command/authorizedprincipals/authorized_principals_test.go deleted file mode 100644 index 2db0d41..0000000 --- a/go/internal/command/authorizedprincipals/authorized_principals_test.go +++ /dev/null @@ -1,47 +0,0 @@ -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/go/internal/command/command.go b/go/internal/command/command.go deleted file mode 100644 index 52393df..0000000 --- a/go/internal/command/command.go +++ /dev/null @@ -1,81 +0,0 @@ -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/go/internal/command/command_test.go b/go/internal/command/command_test.go deleted file mode 100644 index cd3ac9b..0000000 --- a/go/internal/command/command_test.go +++ /dev/null @@ -1,146 +0,0 @@ -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/go/internal/command/commandargs/authorized_keys.go b/go/internal/command/commandargs/authorized_keys.go deleted file mode 100644 index 2733954..0000000 --- a/go/internal/command/commandargs/authorized_keys.go +++ /dev/null @@ -1,51 +0,0 @@ -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 ", 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/go/internal/command/commandargs/authorized_principals.go b/go/internal/command/commandargs/authorized_principals.go deleted file mode 100644 index 746ae3f..0000000 --- a/go/internal/command/commandargs/authorized_principals.go +++ /dev/null @@ -1,50 +0,0 @@ -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 [...]", 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/go/internal/command/commandargs/command_args.go b/go/internal/command/commandargs/command_args.go deleted file mode 100644 index 4831134..0000000 --- a/go/internal/command/commandargs/command_args.go +++ /dev/null @@ -1,31 +0,0 @@ -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/go/internal/command/commandargs/command_args_test.go b/go/internal/command/commandargs/command_args_test.go deleted file mode 100644 index 9f1575d..0000000 --- a/go/internal/command/commandargs/command_args_test.go +++ /dev/null @@ -1,231 +0,0 @@ -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 ", - }, - { - 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 ", - }, - { - 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 [...]", - }, - { - 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/go/internal/command/commandargs/generic_args.go b/go/internal/command/commandargs/generic_args.go deleted file mode 100644 index 96bed99..0000000 --- a/go/internal/command/commandargs/generic_args.go +++ /dev/null @@ -1,14 +0,0 @@ -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/go/internal/command/commandargs/shell.go b/go/internal/command/commandargs/shell.go deleted file mode 100644 index 7e2b72e..0000000 --- a/go/internal/command/commandargs/shell.go +++ /dev/null @@ -1,131 +0,0 @@ -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\d+)\b`) - whoUsernameRegex = regexp.MustCompile(`\busername-(?P\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/go/internal/command/discover/discover.go b/go/internal/command/discover/discover.go deleted file mode 100644 index de94b56..0000000 --- a/go/internal/command/discover/discover.go +++ /dev/null @@ -1,40 +0,0 @@ -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/go/internal/command/discover/discover_test.go b/go/internal/command/discover/discover_test.go deleted file mode 100644 index 7e052f7..0000000 --- a/go/internal/command/discover/discover_test.go +++ /dev/null @@ -1,135 +0,0 @@ -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/go/internal/command/healthcheck/healthcheck.go b/go/internal/command/healthcheck/healthcheck.go deleted file mode 100644 index fef981c..0000000 --- a/go/internal/command/healthcheck/healthcheck.go +++ /dev/null @@ -1,49 +0,0 @@ -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/go/internal/command/healthcheck/healthcheck_test.go b/go/internal/command/healthcheck/healthcheck_test.go deleted file mode 100644 index 6c92ebc..0000000 --- a/go/internal/command/healthcheck/healthcheck_test.go +++ /dev/null @@ -1,90 +0,0 @@ -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/go/internal/command/lfsauthenticate/lfsauthenticate.go b/go/internal/command/lfsauthenticate/lfsauthenticate.go deleted file mode 100644 index bff5e7f..0000000 --- a/go/internal/command/lfsauthenticate/lfsauthenticate.go +++ /dev/null @@ -1,104 +0,0 @@ -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/go/internal/command/lfsauthenticate/lfsauthenticate_test.go b/go/internal/command/lfsauthenticate/lfsauthenticate_test.go deleted file mode 100644 index a6836a8..0000000 --- a/go/internal/command/lfsauthenticate/lfsauthenticate_test.go +++ /dev/null @@ -1,153 +0,0 @@ -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/go/internal/command/readwriter/readwriter.go b/go/internal/command/readwriter/readwriter.go deleted file mode 100644 index da18d30..0000000 --- a/go/internal/command/readwriter/readwriter.go +++ /dev/null @@ -1,9 +0,0 @@ -package readwriter - -import "io" - -type ReadWriter struct { - Out io.Writer - In io.Reader - ErrOut io.Writer -} diff --git a/go/internal/command/receivepack/customaction.go b/go/internal/command/receivepack/customaction.go deleted file mode 100644 index 8623437..0000000 --- a/go/internal/command/receivepack/customaction.go +++ /dev/null @@ -1,99 +0,0 @@ -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/go/internal/command/receivepack/customaction_test.go b/go/internal/command/receivepack/customaction_test.go deleted file mode 100644 index bd4991d..0000000 --- a/go/internal/command/receivepack/customaction_test.go +++ /dev/null @@ -1,105 +0,0 @@ -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/go/internal/command/receivepack/gitalycall.go b/go/internal/command/receivepack/gitalycall.go deleted file mode 100644 index d735f17..0000000 --- a/go/internal/command/receivepack/gitalycall.go +++ /dev/null @@ -1,39 +0,0 @@ -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/go/internal/command/receivepack/gitalycall_test.go b/go/internal/command/receivepack/gitalycall_test.go deleted file mode 100644 index eac9218..0000000 --- a/go/internal/command/receivepack/gitalycall_test.go +++ /dev/null @@ -1,40 +0,0 @@ -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/go/internal/command/receivepack/receivepack.go b/go/internal/command/receivepack/receivepack.go deleted file mode 100644 index eb0b2fe..0000000 --- a/go/internal/command/receivepack/receivepack.go +++ /dev/null @@ -1,40 +0,0 @@ -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/go/internal/command/receivepack/receivepack_test.go b/go/internal/command/receivepack/receivepack_test.go deleted file mode 100644 index a45d054..0000000 --- a/go/internal/command/receivepack/receivepack_test.go +++ /dev/null @@ -1,32 +0,0 @@ -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/go/internal/command/shared/accessverifier/accessverifier.go b/go/internal/command/shared/accessverifier/accessverifier.go deleted file mode 100644 index fc6fa17..0000000 --- a/go/internal/command/shared/accessverifier/accessverifier.go +++ /dev/null @@ -1,45 +0,0 @@ -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/go/internal/command/shared/accessverifier/accessverifier_test.go b/go/internal/command/shared/accessverifier/accessverifier_test.go deleted file mode 100644 index c19ed37..0000000 --- a/go/internal/command/shared/accessverifier/accessverifier_test.go +++ /dev/null @@ -1,82 +0,0 @@ -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/go/internal/command/shared/disallowedcommand/disallowedcommand.go b/go/internal/command/shared/disallowedcommand/disallowedcommand.go deleted file mode 100644 index 3c98bcc..0000000 --- a/go/internal/command/shared/disallowedcommand/disallowedcommand.go +++ /dev/null @@ -1,7 +0,0 @@ -package disallowedcommand - -import "errors" - -var ( - Error = errors.New("> GitLab: Disallowed command") -) diff --git a/go/internal/command/twofactorrecover/twofactorrecover.go b/go/internal/command/twofactorrecover/twofactorrecover.go deleted file mode 100644 index c68080a..0000000 --- a/go/internal/command/twofactorrecover/twofactorrecover.go +++ /dev/null @@ -1,65 +0,0 @@ -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/go/internal/command/twofactorrecover/twofactorrecover_test.go b/go/internal/command/twofactorrecover/twofactorrecover_test.go deleted file mode 100644 index 291d499..0000000 --- a/go/internal/command/twofactorrecover/twofactorrecover_test.go +++ /dev/null @@ -1,136 +0,0 @@ -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/go/internal/command/uploadarchive/gitalycall.go b/go/internal/command/uploadarchive/gitalycall.go deleted file mode 100644 index e810ba3..0000000 --- a/go/internal/command/uploadarchive/gitalycall.go +++ /dev/null @@ -1,32 +0,0 @@ -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/go/internal/command/uploadarchive/gitalycall_test.go b/go/internal/command/uploadarchive/gitalycall_test.go deleted file mode 100644 index 5eb2eae..0000000 --- a/go/internal/command/uploadarchive/gitalycall_test.go +++ /dev/null @@ -1,40 +0,0 @@ -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/go/internal/command/uploadarchive/uploadarchive.go b/go/internal/command/uploadarchive/uploadarchive.go deleted file mode 100644 index 2846455..0000000 --- a/go/internal/command/uploadarchive/uploadarchive.go +++ /dev/null @@ -1,36 +0,0 @@ -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/go/internal/command/uploadarchive/uploadarchive_test.go b/go/internal/command/uploadarchive/uploadarchive_test.go deleted file mode 100644 index 4cd6832..0000000 --- a/go/internal/command/uploadarchive/uploadarchive_test.go +++ /dev/null @@ -1,31 +0,0 @@ -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/go/internal/command/uploadpack/gitalycall.go b/go/internal/command/uploadpack/gitalycall.go deleted file mode 100644 index 5dff24a..0000000 --- a/go/internal/command/uploadpack/gitalycall.go +++ /dev/null @@ -1,36 +0,0 @@ -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/go/internal/command/uploadpack/gitalycall_test.go b/go/internal/command/uploadpack/gitalycall_test.go deleted file mode 100644 index eb18aa8..0000000 --- a/go/internal/command/uploadpack/gitalycall_test.go +++ /dev/null @@ -1,40 +0,0 @@ -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/go/internal/command/uploadpack/uploadpack.go b/go/internal/command/uploadpack/uploadpack.go deleted file mode 100644 index 4b08bf2..0000000 --- a/go/internal/command/uploadpack/uploadpack.go +++ /dev/null @@ -1,36 +0,0 @@ -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/go/internal/command/uploadpack/uploadpack_test.go b/go/internal/command/uploadpack/uploadpack_test.go deleted file mode 100644 index 27a0786..0000000 --- a/go/internal/command/uploadpack/uploadpack_test.go +++ /dev/null @@ -1,31 +0,0 @@ -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()) -} diff --git a/go/internal/config/config.go b/go/internal/config/config.go deleted file mode 100644 index 2231851..0000000 --- a/go/internal/config/config.go +++ /dev/null @@ -1,123 +0,0 @@ -package config - -import ( - "io/ioutil" - "net/url" - "os" - "path" - "path/filepath" - - yaml "gopkg.in/yaml.v2" -) - -const ( - configFile = "config.yml" - logFile = "gitlab-shell.log" - defaultSecretFileName = ".gitlab_shell_secret" -) - -type HttpSettingsConfig struct { - User string `yaml:"user"` - Password string `yaml:"password"` - ReadTimeoutSeconds uint64 `yaml:"read_timeout"` - CaFile string `yaml:"ca_file"` - CaPath string `yaml:"ca_path"` - SelfSignedCert bool `yaml:"self_signed_cert"` -} - -type Config struct { - RootDir string - LogFile string `yaml:"log_file"` - LogFormat string `yaml:"log_format"` - GitlabUrl string `yaml:"gitlab_url"` - GitlabTracing string `yaml:"gitlab_tracing"` - SecretFilePath string `yaml:"secret_file"` - Secret string `yaml:"secret"` - HttpSettings HttpSettingsConfig `yaml:"http_settings"` - HttpClient *HttpClient -} - -func New() (*Config, error) { - dir, err := os.Getwd() - if err != nil { - return nil, err - } - - return NewFromDir(dir) -} - -func NewFromDir(dir string) (*Config, error) { - return newFromFile(path.Join(dir, configFile)) -} - -func newFromFile(filename string) (*Config, error) { - cfg := &Config{RootDir: path.Dir(filename)} - - configBytes, err := ioutil.ReadFile(filename) - if err != nil { - return nil, err - } - - if err := parseConfig(configBytes, cfg); err != nil { - return nil, err - } - - return cfg, nil -} - -// parseConfig expects YAML data in configBytes and a Config instance with RootDir set. -func parseConfig(configBytes []byte, cfg *Config) error { - if err := yaml.Unmarshal(configBytes, cfg); err != nil { - return err - } - - if cfg.LogFile == "" { - cfg.LogFile = logFile - } - - if len(cfg.LogFile) > 0 && cfg.LogFile[0] != '/' { - cfg.LogFile = path.Join(cfg.RootDir, cfg.LogFile) - } - - if cfg.LogFormat == "" { - cfg.LogFormat = "text" - } - - if cfg.GitlabUrl != "" { - unescapedUrl, err := url.PathUnescape(cfg.GitlabUrl) - if err != nil { - return err - } - - cfg.GitlabUrl = unescapedUrl - } - - if err := parseSecret(cfg); err != nil { - return err - } - - return nil -} - -func parseSecret(cfg *Config) error { - // The secret was parsed from yaml no need to read another file - if cfg.Secret != "" { - return nil - } - - if cfg.SecretFilePath == "" { - cfg.SecretFilePath = defaultSecretFileName - } - - if !filepath.IsAbs(cfg.SecretFilePath) { - cfg.SecretFilePath = path.Join(cfg.RootDir, cfg.SecretFilePath) - } - - secretFileContent, err := ioutil.ReadFile(cfg.SecretFilePath) - if err != nil { - return err - } - cfg.Secret = string(secretFileContent) - - return nil -} diff --git a/go/internal/config/config_test.go b/go/internal/config/config_test.go deleted file mode 100644 index e31ff70..0000000 --- a/go/internal/config/config_test.go +++ /dev/null @@ -1,112 +0,0 @@ -package config - -import ( - "fmt" - "path" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" -) - -const ( - customSecret = "custom/my-contents-is-secret" -) - -var ( - testRoot = testhelper.TestRoot -) - -func TestParseConfig(t *testing.T) { - cleanup, err := testhelper.PrepareTestRootDir() - require.NoError(t, err) - defer cleanup() - - testCases := []struct { - yaml string - path string - format string - gitlabUrl string - secret string - httpSettings HttpSettingsConfig - }{ - { - path: path.Join(testRoot, "gitlab-shell.log"), - format: "text", - secret: "default-secret-content", - }, - { - yaml: "log_file: my-log.log", - path: path.Join(testRoot, "my-log.log"), - format: "text", - secret: "default-secret-content", - }, - { - yaml: "log_file: /qux/my-log.log", - path: "/qux/my-log.log", - format: "text", - secret: "default-secret-content", - }, - { - yaml: "log_format: json", - path: path.Join(testRoot, "gitlab-shell.log"), - format: "json", - secret: "default-secret-content", - }, - { - yaml: "gitlab_url: http+unix://%2Fpath%2Fto%2Fgitlab%2Fgitlab.socket", - path: path.Join(testRoot, "gitlab-shell.log"), - format: "text", - gitlabUrl: "http+unix:///path/to/gitlab/gitlab.socket", - secret: "default-secret-content", - }, - { - yaml: fmt.Sprintf("secret_file: %s", customSecret), - path: path.Join(testRoot, "gitlab-shell.log"), - format: "text", - secret: "custom-secret-content", - }, - { - yaml: fmt.Sprintf("secret_file: %s", path.Join(testRoot, customSecret)), - path: path.Join(testRoot, "gitlab-shell.log"), - format: "text", - secret: "custom-secret-content", - }, - { - yaml: "secret: an inline secret", - path: path.Join(testRoot, "gitlab-shell.log"), - format: "text", - secret: "an inline secret", - }, - { - yaml: "http_settings:\n user: user_basic_auth\n password: password_basic_auth\n read_timeout: 500", - path: path.Join(testRoot, "gitlab-shell.log"), - format: "text", - secret: "default-secret-content", - httpSettings: HttpSettingsConfig{User: "user_basic_auth", Password: "password_basic_auth", ReadTimeoutSeconds: 500}, - }, - { - yaml: "http_settings:\n ca_file: /etc/ssl/cert.pem\n ca_path: /etc/pki/tls/certs\n self_signed_cert: true", - path: path.Join(testRoot, "gitlab-shell.log"), - format: "text", - secret: "default-secret-content", - httpSettings: HttpSettingsConfig{CaFile: "/etc/ssl/cert.pem", CaPath: "/etc/pki/tls/certs", SelfSignedCert: true}, - }, - } - - for _, tc := range testCases { - t.Run(fmt.Sprintf("yaml input: %q", tc.yaml), func(t *testing.T) { - cfg := Config{RootDir: testRoot} - - err := parseConfig([]byte(tc.yaml), &cfg) - require.NoError(t, err) - - assert.Equal(t, tc.path, cfg.LogFile) - assert.Equal(t, tc.format, cfg.LogFormat) - assert.Equal(t, tc.gitlabUrl, cfg.GitlabUrl) - assert.Equal(t, tc.secret, cfg.Secret) - assert.Equal(t, tc.httpSettings, cfg.HttpSettings) - }) - } -} diff --git a/go/internal/config/httpclient.go b/go/internal/config/httpclient.go deleted file mode 100644 index c71efad..0000000 --- a/go/internal/config/httpclient.go +++ /dev/null @@ -1,122 +0,0 @@ -package config - -import ( - "context" - "crypto/tls" - "crypto/x509" - "io/ioutil" - "net" - "net/http" - "path/filepath" - "strings" - "time" -) - -const ( - socketBaseUrl = "http://unix" - unixSocketProtocol = "http+unix://" - httpProtocol = "http://" - httpsProtocol = "https://" - defaultReadTimeoutSeconds = 300 -) - -type HttpClient struct { - HttpClient *http.Client - Host string -} - -func (c *Config) GetHttpClient() *HttpClient { - if c.HttpClient != nil { - return c.HttpClient - } - - var transport *http.Transport - var host string - if strings.HasPrefix(c.GitlabUrl, unixSocketProtocol) { - transport, host = c.buildSocketTransport() - } else if strings.HasPrefix(c.GitlabUrl, httpProtocol) { - transport, host = c.buildHttpTransport() - } else if strings.HasPrefix(c.GitlabUrl, httpsProtocol) { - transport, host = c.buildHttpsTransport() - } else { - return nil - } - - httpClient := &http.Client{ - Transport: transport, - Timeout: c.readTimeout(), - } - - client := &HttpClient{HttpClient: httpClient, Host: host} - - c.HttpClient = client - - return client -} - -func (c *Config) buildSocketTransport() (*http.Transport, string) { - socketPath := strings.TrimPrefix(c.GitlabUrl, unixSocketProtocol) - transport := &http.Transport{ - DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { - dialer := net.Dialer{} - return dialer.DialContext(ctx, "unix", socketPath) - }, - } - - return transport, socketBaseUrl -} - -func (c *Config) buildHttpsTransport() (*http.Transport, string) { - certPool, err := x509.SystemCertPool() - - if err != nil { - certPool = x509.NewCertPool() - } - - caFile := c.HttpSettings.CaFile - if caFile != "" { - addCertToPool(certPool, caFile) - } - - caPath := c.HttpSettings.CaPath - if caPath != "" { - fis, _ := ioutil.ReadDir(caPath) - for _, fi := range fis { - if fi.IsDir() { - continue - } - - addCertToPool(certPool, filepath.Join(caPath, fi.Name())) - } - } - - transport := &http.Transport{ - TLSClientConfig: &tls.Config{ - RootCAs: certPool, - InsecureSkipVerify: c.HttpSettings.SelfSignedCert, - }, - } - - return transport, c.GitlabUrl -} - -func addCertToPool(certPool *x509.CertPool, fileName string) { - cert, err := ioutil.ReadFile(fileName) - if err == nil { - certPool.AppendCertsFromPEM(cert) - } -} - -func (c *Config) buildHttpTransport() (*http.Transport, string) { - return &http.Transport{}, c.GitlabUrl -} - -func (c *Config) readTimeout() time.Duration { - timeoutSeconds := c.HttpSettings.ReadTimeoutSeconds - - if timeoutSeconds == 0 { - timeoutSeconds = defaultReadTimeoutSeconds - } - - return time.Duration(timeoutSeconds) * time.Second -} diff --git a/go/internal/config/httpclient_test.go b/go/internal/config/httpclient_test.go deleted file mode 100644 index 474deba..0000000 --- a/go/internal/config/httpclient_test.go +++ /dev/null @@ -1,22 +0,0 @@ -package config - -import ( - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestReadTimeout(t *testing.T) { - expectedSeconds := uint64(300) - - config := &Config{ - GitlabUrl: "http://localhost:3000", - HttpSettings: HttpSettingsConfig{ReadTimeoutSeconds: expectedSeconds}, - } - client := config.GetHttpClient() - - require.NotNil(t, client) - assert.Equal(t, time.Duration(expectedSeconds)*time.Second, client.HttpClient.Timeout) -} diff --git a/go/internal/executable/executable.go b/go/internal/executable/executable.go deleted file mode 100644 index c6355b9..0000000 --- a/go/internal/executable/executable.go +++ /dev/null @@ -1,60 +0,0 @@ -package executable - -import ( - "os" - "path/filepath" -) - -const ( - BinDir = "bin" - Healthcheck = "check" - GitlabShell = "gitlab-shell" - AuthorizedKeysCheck = "gitlab-shell-authorized-keys-check" - AuthorizedPrincipalsCheck = "gitlab-shell-authorized-principals-check" -) - -type Executable struct { - Name string - RootDir string -} - -var ( - // osExecutable is overridden in tests - osExecutable = os.Executable -) - -func New(name string) (*Executable, error) { - path, err := osExecutable() - if err != nil { - return nil, err - } - - rootDir, err := findRootDir(path) - if err != nil { - return nil, err - } - - executable := &Executable{ - Name: name, - RootDir: rootDir, - } - - return executable, nil -} - -func findRootDir(path string) (string, error) { - // Start: /opt/.../gitlab-shell/bin/gitlab-shell - // Ends: /opt/.../gitlab-shell - rootDir := filepath.Dir(filepath.Dir(path)) - pathFromEnv := os.Getenv("GITLAB_SHELL_DIR") - - if pathFromEnv != "" { - if _, err := os.Stat(pathFromEnv); os.IsNotExist(err) { - return "", err - } - - rootDir = pathFromEnv - } - - return rootDir, nil -} diff --git a/go/internal/executable/executable_test.go b/go/internal/executable/executable_test.go deleted file mode 100644 index 581821d..0000000 --- a/go/internal/executable/executable_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package executable - -import ( - "errors" - "testing" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" - - "github.com/stretchr/testify/require" -) - -type fakeOs struct { - OldExecutable func() (string, error) - Path string - Error error -} - -func (f *fakeOs) Executable() (string, error) { - return f.Path, f.Error -} - -func (f *fakeOs) Setup() { - f.OldExecutable = osExecutable - osExecutable = f.Executable -} - -func (f *fakeOs) Cleanup() { - osExecutable = f.OldExecutable -} - -func TestNewSuccess(t *testing.T) { - testCases := []struct { - desc string - fakeOs *fakeOs - environment map[string]string - expectedRootDir string - }{ - { - desc: "GITLAB_SHELL_DIR env var is not defined", - fakeOs: &fakeOs{Path: "/tmp/bin/gitlab-shell"}, - expectedRootDir: "/tmp", - }, - { - desc: "GITLAB_SHELL_DIR env var is defined", - fakeOs: &fakeOs{Path: "/opt/bin/gitlab-shell"}, - environment: map[string]string{ - "GITLAB_SHELL_DIR": "/tmp", - }, - expectedRootDir: "/tmp", - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - restoreEnv := testhelper.TempEnv(tc.environment) - defer restoreEnv() - - fake := tc.fakeOs - fake.Setup() - defer fake.Cleanup() - - result, err := New("gitlab-shell") - - require.NoError(t, err) - require.Equal(t, result.Name, "gitlab-shell") - require.Equal(t, result.RootDir, tc.expectedRootDir) - }) - } -} - -func TestNewFailure(t *testing.T) { - testCases := []struct { - desc string - fakeOs *fakeOs - environment map[string]string - }{ - { - desc: "failed to determine executable", - fakeOs: &fakeOs{Path: "", Error: errors.New("error")}, - }, - { - desc: "GITLAB_SHELL_DIR doesn't exist", - fakeOs: &fakeOs{Path: "/tmp/bin/gitlab-shell"}, - environment: map[string]string{ - "GITLAB_SHELL_DIR": "/tmp/non/existing/directory", - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - restoreEnv := testhelper.TempEnv(tc.environment) - defer restoreEnv() - - fake := tc.fakeOs - fake.Setup() - defer fake.Cleanup() - - _, err := New("gitlab-shell") - - require.Error(t, err) - }) - } -} diff --git a/go/internal/gitlabnet/accessverifier/client.go b/go/internal/gitlabnet/accessverifier/client.go deleted file mode 100644 index eb67703..0000000 --- a/go/internal/gitlabnet/accessverifier/client.go +++ /dev/null @@ -1,115 +0,0 @@ -package accessverifier - -import ( - "fmt" - "net/http" - - 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/config" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/sshenv" -) - -const ( - protocol = "ssh" - anyChanges = "_any" -) - -type Client struct { - client *gitlabnet.GitlabClient -} - -type Request struct { - Action commandargs.CommandType `json:"action"` - Repo string `json:"project"` - Changes string `json:"changes"` - Protocol string `json:"protocol"` - KeyId string `json:"key_id,omitempty"` - Username string `json:"username,omitempty"` - CheckIp string `json:"check_ip,omitempty"` -} - -type Gitaly struct { - Repo pb.Repository `json:"repository"` - Address string `json:"address"` - Token string `json:"token"` -} - -type CustomPayloadData struct { - ApiEndpoints []string `json:"api_endpoints"` - Username string `json:"gl_username"` - PrimaryRepo string `json:"primary_repo"` - InfoMessage string `json:"info_message"` - UserId string `json:"gl_id,omitempty"` -} - -type CustomPayload struct { - Action string `json:"action"` - Data CustomPayloadData `json:"data"` -} - -type Response struct { - Success bool `json:"status"` - Message string `json:"message"` - Repo string `json:"gl_repository"` - UserId string `json:"gl_id"` - Username string `json:"gl_username"` - GitConfigOptions []string `json:"git_config_options"` - Gitaly Gitaly `json:"gitaly"` - GitProtocol string `json:"git_protocol"` - Payload CustomPayload `json:"payload"` - ConsoleMessages []string `json:"gl_console_messages"` - Who string - StatusCode int -} - -func NewClient(config *config.Config) (*Client, error) { - client, err := gitlabnet.GetClient(config) - if err != nil { - return nil, fmt.Errorf("Error creating http client: %v", err) - } - - return &Client{client: client}, nil -} - -func (c *Client) Verify(args *commandargs.Shell, action commandargs.CommandType, repo string) (*Response, error) { - request := &Request{Action: action, Repo: repo, Protocol: protocol, Changes: anyChanges} - - if args.GitlabUsername != "" { - request.Username = args.GitlabUsername - } else { - request.KeyId = args.GitlabKeyId - } - - request.CheckIp = sshenv.LocalAddr() - - response, err := c.client.Post("/allowed", request) - if err != nil { - return nil, err - } - defer response.Body.Close() - - return parse(response, args) -} - -func parse(hr *http.Response, args *commandargs.Shell) (*Response, error) { - response := &Response{} - if err := gitlabnet.ParseJSON(hr, response); err != nil { - return nil, err - } - - if args.GitlabKeyId != "" { - response.Who = "key-" + args.GitlabKeyId - } else { - response.Who = response.UserId - } - - response.StatusCode = hr.StatusCode - - return response, nil -} - -func (r *Response) IsCustomAction() bool { - return r.StatusCode == http.StatusMultipleChoices -} diff --git a/go/internal/gitlabnet/accessverifier/client_test.go b/go/internal/gitlabnet/accessverifier/client_test.go deleted file mode 100644 index a4d1cb4..0000000 --- a/go/internal/gitlabnet/accessverifier/client_test.go +++ /dev/null @@ -1,209 +0,0 @@ -package accessverifier - -import ( - "encoding/json" - "io/ioutil" - "net/http" - "path" - "testing" - - "github.com/stretchr/testify/require" - - 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/config" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" -) - -var ( - repo = "group/private" - action = commandargs.ReceivePack -) - -func buildExpectedResponse(who string) *Response { - response := &Response{ - Success: true, - UserId: "user-1", - Repo: "project-26", - Username: "root", - GitConfigOptions: []string{"option"}, - Gitaly: Gitaly{ - Repo: pb.Repository{ - StorageName: "default", - RelativePath: "@hashed/5f/9c/5f9c4ab08cac7457e9111a30e4664920607ea2c115a1433d7be98e97e64244ca.git", - GitObjectDirectory: "path/to/git_object_directory", - GitAlternateObjectDirectories: []string{"path/to/git_alternate_object_directory"}, - GlRepository: "project-26", - GlProjectPath: repo, - }, - Address: "unix:gitaly.socket", - Token: "token", - }, - GitProtocol: "protocol", - Payload: CustomPayload{}, - ConsoleMessages: []string{"console", "message"}, - Who: who, - StatusCode: 200, - } - - return response -} - -func TestSuccessfulResponses(t *testing.T) { - client, cleanup := setup(t) - defer cleanup() - - testCases := []struct { - desc string - args *commandargs.Shell - who string - }{ - { - desc: "Provide key id within the request", - args: &commandargs.Shell{GitlabKeyId: "1"}, - who: "key-1", - }, { - desc: "Provide username within the request", - args: &commandargs.Shell{GitlabUsername: "first"}, - who: "user-1", - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - result, err := client.Verify(tc.args, action, repo) - require.NoError(t, err) - - response := buildExpectedResponse(tc.who) - require.Equal(t, response, result) - }) - } -} - -func TestGetCustomAction(t *testing.T) { - client, cleanup := setup(t) - defer cleanup() - - args := &commandargs.Shell{GitlabUsername: "custom"} - result, err := client.Verify(args, action, repo) - require.NoError(t, err) - - response := buildExpectedResponse("user-1") - response.Payload = CustomPayload{ - Action: "geo_proxy_to_primary", - Data: CustomPayloadData{ - ApiEndpoints: []string{"geo/proxy_git_push_ssh/info_refs", "geo/proxy_git_push_ssh/push"}, - Username: "custom", - PrimaryRepo: "https://repo/path", - InfoMessage: "message", - }, - } - response.StatusCode = 300 - - require.True(t, response.IsCustomAction()) - require.Equal(t, response, result) -} - -func TestErrorResponses(t *testing.T) { - client, cleanup := setup(t) - defer cleanup() - - testCases := []struct { - desc string - fakeId string - expectedError string - }{ - { - desc: "A response with an error message", - fakeId: "2", - expectedError: "Not allowed!", - }, - { - desc: "A response with bad JSON", - fakeId: "3", - expectedError: "Parsing failed", - }, - { - desc: "An error response without message", - fakeId: "4", - expectedError: "Internal API error (403)", - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - args := &commandargs.Shell{GitlabKeyId: tc.fakeId} - resp, err := client.Verify(args, action, repo) - - require.EqualError(t, err, tc.expectedError) - require.Nil(t, resp) - }) - } -} - -func setup(t *testing.T) (*Client, func()) { - testDirCleanup, err := testhelper.PrepareTestRootDir() - require.NoError(t, err) - defer testDirCleanup() - - body, err := ioutil.ReadFile(path.Join(testhelper.TestRoot, "responses/allowed.json")) - require.NoError(t, err) - - allowedWithPayloadPath := path.Join(testhelper.TestRoot, "responses/allowed_with_payload.json") - bodyWithPayload, err := ioutil.ReadFile(allowedWithPayloadPath) - require.NoError(t, err) - - requests := []testserver.TestRequestHandler{ - { - Path: "/api/v4/internal/allowed", - Handler: func(w http.ResponseWriter, r *http.Request) { - b, err := ioutil.ReadAll(r.Body) - require.NoError(t, err) - - var requestBody *Request - require.NoError(t, json.Unmarshal(b, &requestBody)) - - switch requestBody.Username { - case "first": - _, err = w.Write(body) - require.NoError(t, err) - case "second": - errBody := map[string]interface{}{ - "status": false, - "message": "missing user", - } - require.NoError(t, json.NewEncoder(w).Encode(errBody)) - case "custom": - w.WriteHeader(http.StatusMultipleChoices) - _, err = w.Write(bodyWithPayload) - require.NoError(t, err) - } - - switch requestBody.KeyId { - case "1": - _, err = w.Write(body) - require.NoError(t, err) - case "2": - w.WriteHeader(http.StatusForbidden) - errBody := &gitlabnet.ErrorResponse{ - Message: "Not allowed!", - } - require.NoError(t, json.NewEncoder(w).Encode(errBody)) - case "3": - w.Write([]byte("{ \"message\": \"broken json!\"")) - case "4": - w.WriteHeader(http.StatusForbidden) - } - }, - }, - } - - url, cleanup := testserver.StartSocketHttpServer(t, requests) - - client, err := NewClient(&config.Config{GitlabUrl: url}) - require.NoError(t, err) - - return client, cleanup -} diff --git a/go/internal/gitlabnet/authorizedkeys/client.go b/go/internal/gitlabnet/authorizedkeys/client.go deleted file mode 100644 index 28b85fc..0000000 --- a/go/internal/gitlabnet/authorizedkeys/client.go +++ /dev/null @@ -1,65 +0,0 @@ -package authorizedkeys - -import ( - "fmt" - "net/url" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" -) - -const ( - AuthorizedKeysPath = "/authorized_keys" -) - -type Client struct { - config *config.Config - client *gitlabnet.GitlabClient -} - -type Response struct { - Id int64 `json:"id"` - Key string `json:"key"` -} - -func NewClient(config *config.Config) (*Client, error) { - client, err := gitlabnet.GetClient(config) - if err != nil { - return nil, fmt.Errorf("Error creating http client: %v", err) - } - - return &Client{config: config, client: client}, nil -} - -func (c *Client) GetByKey(key string) (*Response, error) { - path, err := pathWithKey(key) - if err != nil { - return nil, err - } - - response, err := c.client.Get(path) - if err != nil { - return nil, err - } - defer response.Body.Close() - - parsedResponse := &Response{} - if err := gitlabnet.ParseJSON(response, parsedResponse); err != nil { - return nil, err - } - - return parsedResponse, nil -} - -func pathWithKey(key string) (string, error) { - u, err := url.Parse(AuthorizedKeysPath) - if err != nil { - return "", err - } - - params := u.Query() - params.Set("key", key) - u.RawQuery = params.Encode() - - return u.String(), nil -} diff --git a/go/internal/gitlabnet/authorizedkeys/client_test.go b/go/internal/gitlabnet/authorizedkeys/client_test.go deleted file mode 100644 index c73aab2..0000000 --- a/go/internal/gitlabnet/authorizedkeys/client_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package authorizedkeys - -import ( - "encoding/json" - "net/http" - "testing" - - "github.com/stretchr/testify/require" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" -) - -var ( - requests []testserver.TestRequestHandler -) - -func init() { - 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 := &Response{ - Id: 1, - Key: "public-key", - } - json.NewEncoder(w).Encode(body) - } else if r.URL.Query().Get("key") == "broken-message" { - w.WriteHeader(http.StatusForbidden) - body := &gitlabnet.ErrorResponse{ - Message: "Not allowed!", - } - json.NewEncoder(w).Encode(body) - } else if r.URL.Query().Get("key") == "broken-json" { - w.Write([]byte("{ \"message\": \"broken json!\"")) - } else if r.URL.Query().Get("key") == "broken-empty" { - w.WriteHeader(http.StatusForbidden) - } else { - w.WriteHeader(http.StatusNotFound) - } - }, - }, - } -} - -func TestGetByKey(t *testing.T) { - client, cleanup := setup(t) - defer cleanup() - - result, err := client.GetByKey("key") - require.NoError(t, err) - require.Equal(t, &Response{Id: 1, Key: "public-key"}, result) -} - -func TestGetByKeyErrorResponses(t *testing.T) { - client, cleanup := setup(t) - defer cleanup() - - testCases := []struct { - desc string - key string - expectedError string - }{ - { - desc: "A response with an error message", - key: "broken-message", - expectedError: "Not allowed!", - }, - { - desc: "A response with bad JSON", - key: "broken-json", - expectedError: "Parsing failed", - }, - { - desc: "A forbidden (403) response without message", - key: "broken-empty", - expectedError: "Internal API error (403)", - }, - { - desc: "A not found (404) response without message", - key: "not-found", - expectedError: "Internal API error (404)", - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - resp, err := client.GetByKey(tc.key) - - require.EqualError(t, err, tc.expectedError) - require.Nil(t, resp) - }) - } -} - -func setup(t *testing.T) (*Client, func()) { - url, cleanup := testserver.StartSocketHttpServer(t, requests) - - client, err := NewClient(&config.Config{GitlabUrl: url}) - require.NoError(t, err) - - return client, cleanup -} diff --git a/go/internal/gitlabnet/client.go b/go/internal/gitlabnet/client.go deleted file mode 100644 index dacb1d6..0000000 --- a/go/internal/gitlabnet/client.go +++ /dev/null @@ -1,132 +0,0 @@ -package gitlabnet - -import ( - "bytes" - "encoding/base64" - "encoding/json" - "fmt" - "io" - "net/http" - "strings" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" -) - -const ( - internalApiPath = "/api/v4/internal" - secretHeaderName = "Gitlab-Shared-Secret" -) - -var ( - ParsingError = fmt.Errorf("Parsing failed") -) - -type ErrorResponse struct { - Message string `json:"message"` -} - -type GitlabClient struct { - httpClient *http.Client - config *config.Config - host string -} - -func GetClient(config *config.Config) (*GitlabClient, error) { - client := config.GetHttpClient() - - if client == nil { - return nil, fmt.Errorf("Unsupported protocol") - } - - return &GitlabClient{httpClient: client.HttpClient, config: config, host: client.Host}, nil -} - -func normalizePath(path string) string { - if !strings.HasPrefix(path, "/") { - path = "/" + path - } - - if !strings.HasPrefix(path, internalApiPath) { - path = internalApiPath + path - } - return path -} - -func newRequest(method, host, path string, data interface{}) (*http.Request, error) { - var jsonReader io.Reader - if data != nil { - jsonData, err := json.Marshal(data) - if err != nil { - return nil, err - } - - jsonReader = bytes.NewReader(jsonData) - } - - request, err := http.NewRequest(method, host+path, jsonReader) - if err != nil { - return nil, err - } - - return request, nil -} - -func parseError(resp *http.Response) error { - if resp.StatusCode >= 200 && resp.StatusCode <= 399 { - return nil - } - defer resp.Body.Close() - parsedResponse := &ErrorResponse{} - - if err := json.NewDecoder(resp.Body).Decode(parsedResponse); err != nil { - return fmt.Errorf("Internal API error (%v)", resp.StatusCode) - } else { - return fmt.Errorf(parsedResponse.Message) - } - -} - -func (c *GitlabClient) Get(path string) (*http.Response, error) { - return c.DoRequest(http.MethodGet, normalizePath(path), nil) -} - -func (c *GitlabClient) Post(path string, data interface{}) (*http.Response, error) { - return c.DoRequest(http.MethodPost, normalizePath(path), data) -} - -func (c *GitlabClient) DoRequest(method, path string, data interface{}) (*http.Response, error) { - request, err := newRequest(method, c.host, path, data) - if err != nil { - return nil, err - } - - user, password := c.config.HttpSettings.User, c.config.HttpSettings.Password - if user != "" && password != "" { - request.SetBasicAuth(user, password) - } - - encodedSecret := base64.StdEncoding.EncodeToString([]byte(c.config.Secret)) - request.Header.Set(secretHeaderName, encodedSecret) - - request.Header.Add("Content-Type", "application/json") - request.Close = true - - response, err := c.httpClient.Do(request) - if err != nil { - return nil, fmt.Errorf("Internal API unreachable") - } - - if err := parseError(response); err != nil { - return nil, err - } - - return response, nil -} - -func ParseJSON(hr *http.Response, response interface{}) error { - if err := json.NewDecoder(hr.Body).Decode(response); err != nil { - return ParsingError - } - - return nil -} diff --git a/go/internal/gitlabnet/client_test.go b/go/internal/gitlabnet/client_test.go deleted file mode 100644 index e8499dc..0000000 --- a/go/internal/gitlabnet/client_test.go +++ /dev/null @@ -1,219 +0,0 @@ -package gitlabnet - -import ( - "encoding/base64" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "path" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" -) - -func TestClients(t *testing.T) { - testDirCleanup, err := testhelper.PrepareTestRootDir() - require.NoError(t, err) - defer testDirCleanup() - - requests := []testserver.TestRequestHandler{ - { - Path: "/api/v4/internal/hello", - Handler: func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - - fmt.Fprint(w, "Hello") - }, - }, - { - Path: "/api/v4/internal/post_endpoint", - Handler: func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - - b, err := ioutil.ReadAll(r.Body) - defer r.Body.Close() - - require.NoError(t, err) - - fmt.Fprint(w, "Echo: "+string(b)) - }, - }, - { - Path: "/api/v4/internal/auth", - Handler: func(w http.ResponseWriter, r *http.Request) { - fmt.Fprint(w, r.Header.Get(secretHeaderName)) - }, - }, - { - Path: "/api/v4/internal/error", - Handler: func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(http.StatusBadRequest) - body := map[string]string{ - "message": "Don't do that", - } - json.NewEncoder(w).Encode(body) - }, - }, - { - Path: "/api/v4/internal/broken", - Handler: func(w http.ResponseWriter, r *http.Request) { - panic("Broken") - }, - }, - } - - testCases := []struct { - desc string - config *config.Config - server func(*testing.T, []testserver.TestRequestHandler) (string, func()) - }{ - { - desc: "Socket client", - config: &config.Config{}, - server: testserver.StartSocketHttpServer, - }, - { - desc: "Http client", - config: &config.Config{}, - server: testserver.StartHttpServer, - }, - { - desc: "Https client", - config: &config.Config{ - HttpSettings: config.HttpSettingsConfig{CaFile: path.Join(testhelper.TestRoot, "certs/valid/server.crt")}, - }, - server: testserver.StartHttpsServer, - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - url, cleanup := tc.server(t, requests) - defer cleanup() - - tc.config.GitlabUrl = url - tc.config.Secret = "sssh, it's a secret" - - client, err := GetClient(tc.config) - require.NoError(t, err) - - testBrokenRequest(t, client) - testSuccessfulGet(t, client) - testSuccessfulPost(t, client) - testMissing(t, client) - testErrorMessage(t, client) - testAuthenticationHeader(t, client) - }) - } -} - -func testSuccessfulGet(t *testing.T, client *GitlabClient) { - t.Run("Successful get", func(t *testing.T) { - response, err := client.Get("/hello") - require.NoError(t, err) - require.NotNil(t, response) - - defer response.Body.Close() - - responseBody, err := ioutil.ReadAll(response.Body) - assert.NoError(t, err) - assert.Equal(t, string(responseBody), "Hello") - }) -} - -func testSuccessfulPost(t *testing.T, client *GitlabClient) { - t.Run("Successful Post", func(t *testing.T) { - data := map[string]string{"key": "value"} - - response, err := client.Post("/post_endpoint", data) - require.NoError(t, err) - require.NotNil(t, response) - - defer response.Body.Close() - - responseBody, err := ioutil.ReadAll(response.Body) - assert.NoError(t, err) - assert.Equal(t, "Echo: {\"key\":\"value\"}", string(responseBody)) - }) -} - -func testMissing(t *testing.T, client *GitlabClient) { - t.Run("Missing error for GET", func(t *testing.T) { - response, err := client.Get("/missing") - assert.EqualError(t, err, "Internal API error (404)") - assert.Nil(t, response) - }) - - t.Run("Missing error for POST", func(t *testing.T) { - response, err := client.Post("/missing", map[string]string{}) - assert.EqualError(t, err, "Internal API error (404)") - assert.Nil(t, response) - }) -} - -func testErrorMessage(t *testing.T, client *GitlabClient) { - t.Run("Error with message for GET", func(t *testing.T) { - response, err := client.Get("/error") - assert.EqualError(t, err, "Don't do that") - assert.Nil(t, response) - }) - - t.Run("Error with message for POST", func(t *testing.T) { - response, err := client.Post("/error", map[string]string{}) - assert.EqualError(t, err, "Don't do that") - assert.Nil(t, response) - }) -} - -func testBrokenRequest(t *testing.T, client *GitlabClient) { - t.Run("Broken request for GET", func(t *testing.T) { - response, err := client.Get("/broken") - assert.EqualError(t, err, "Internal API unreachable") - assert.Nil(t, response) - }) - - t.Run("Broken request for POST", func(t *testing.T) { - response, err := client.Post("/broken", map[string]string{}) - assert.EqualError(t, err, "Internal API unreachable") - assert.Nil(t, response) - }) -} - -func testAuthenticationHeader(t *testing.T, client *GitlabClient) { - t.Run("Authentication headers for GET", func(t *testing.T) { - response, err := client.Get("/auth") - require.NoError(t, err) - require.NotNil(t, response) - - defer response.Body.Close() - - responseBody, err := ioutil.ReadAll(response.Body) - require.NoError(t, err) - - header, err := base64.StdEncoding.DecodeString(string(responseBody)) - require.NoError(t, err) - assert.Equal(t, "sssh, it's a secret", string(header)) - }) - - t.Run("Authentication headers for POST", func(t *testing.T) { - response, err := client.Post("/auth", map[string]string{}) - require.NoError(t, err) - require.NotNil(t, response) - - defer response.Body.Close() - - responseBody, err := ioutil.ReadAll(response.Body) - require.NoError(t, err) - - header, err := base64.StdEncoding.DecodeString(string(responseBody)) - require.NoError(t, err) - assert.Equal(t, "sssh, it's a secret", string(header)) - }) -} diff --git a/go/internal/gitlabnet/discover/client.go b/go/internal/gitlabnet/discover/client.go deleted file mode 100644 index 46ab2de..0000000 --- a/go/internal/gitlabnet/discover/client.go +++ /dev/null @@ -1,71 +0,0 @@ -package discover - -import ( - "fmt" - "net/http" - "net/url" - - "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 -} - -type Response struct { - UserId int64 `json:"id"` - Name string `json:"name"` - Username string `json:"username"` -} - -func NewClient(config *config.Config) (*Client, error) { - client, err := gitlabnet.GetClient(config) - if err != nil { - return nil, fmt.Errorf("Error creating http client: %v", err) - } - - return &Client{config: config, client: client}, nil -} - -func (c *Client) GetByCommandArgs(args *commandargs.Shell) (*Response, error) { - params := url.Values{} - if args.GitlabUsername != "" { - params.Add("username", args.GitlabUsername) - } else if args.GitlabKeyId != "" { - params.Add("key_id", args.GitlabKeyId) - } else { - // There was no 'who' information, this matches the ruby error - // message. - return nil, fmt.Errorf("who='' is invalid") - } - - return c.getResponse(params) -} - -func (c *Client) getResponse(params url.Values) (*Response, error) { - path := "/discover?" + params.Encode() - - response, err := c.client.Get(path) - 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 -} - -func (r *Response) IsAnonymous() bool { - return r.UserId < 1 -} diff --git a/go/internal/gitlabnet/discover/client_test.go b/go/internal/gitlabnet/discover/client_test.go deleted file mode 100644 index b98a28e..0000000 --- a/go/internal/gitlabnet/discover/client_test.go +++ /dev/null @@ -1,137 +0,0 @@ -package discover - -import ( - "encoding/json" - "fmt" - "net/http" - "net/url" - "testing" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -var ( - requests []testserver.TestRequestHandler -) - -func init() { - requests = []testserver.TestRequestHandler{ - { - Path: "/api/v4/internal/discover", - Handler: func(w http.ResponseWriter, r *http.Request) { - if r.URL.Query().Get("key_id") == "1" { - body := &Response{ - UserId: 2, - Username: "alex-doe", - Name: "Alex Doe", - } - json.NewEncoder(w).Encode(body) - } else if r.URL.Query().Get("username") == "jane-doe" { - body := &Response{ - UserId: 1, - Username: "jane-doe", - Name: "Jane Doe", - } - json.NewEncoder(w).Encode(body) - } else if r.URL.Query().Get("username") == "broken_message" { - w.WriteHeader(http.StatusForbidden) - body := &gitlabnet.ErrorResponse{ - Message: "Not allowed!", - } - json.NewEncoder(w).Encode(body) - } else if r.URL.Query().Get("username") == "broken_json" { - w.Write([]byte("{ \"message\": \"broken json!\"")) - } else if r.URL.Query().Get("username") == "broken_empty" { - w.WriteHeader(http.StatusForbidden) - } else { - fmt.Fprint(w, "null") - } - }, - }, - } -} - -func TestGetByKeyId(t *testing.T) { - client, cleanup := setup(t) - defer cleanup() - - params := url.Values{} - params.Add("key_id", "1") - result, err := client.getResponse(params) - assert.NoError(t, err) - assert.Equal(t, &Response{UserId: 2, Username: "alex-doe", Name: "Alex Doe"}, result) -} - -func TestGetByUsername(t *testing.T) { - client, cleanup := setup(t) - defer cleanup() - - params := url.Values{} - params.Add("username", "jane-doe") - result, err := client.getResponse(params) - assert.NoError(t, err) - assert.Equal(t, &Response{UserId: 1, Username: "jane-doe", Name: "Jane Doe"}, result) -} - -func TestMissingUser(t *testing.T) { - client, cleanup := setup(t) - defer cleanup() - - params := url.Values{} - params.Add("username", "missing") - result, err := client.getResponse(params) - assert.NoError(t, err) - assert.True(t, result.IsAnonymous()) -} - -func TestErrorResponses(t *testing.T) { - client, cleanup := setup(t) - defer cleanup() - - testCases := []struct { - desc string - fakeUsername string - expectedError string - }{ - { - desc: "A response with an error message", - fakeUsername: "broken_message", - expectedError: "Not allowed!", - }, - { - desc: "A response with bad JSON", - fakeUsername: "broken_json", - expectedError: "Parsing failed", - }, - { - desc: "An error response without message", - fakeUsername: "broken_empty", - expectedError: "Internal API error (403)", - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - params := url.Values{} - params.Add("username", tc.fakeUsername) - resp, err := client.getResponse(params) - - assert.EqualError(t, err, tc.expectedError) - assert.Nil(t, resp) - }) - } -} - -func setup(t *testing.T) (*Client, func()) { - url, cleanup := testserver.StartSocketHttpServer(t, requests) - - client, err := NewClient(&config.Config{GitlabUrl: url}) - require.NoError(t, err) - - return client, cleanup -} diff --git a/go/internal/gitlabnet/healthcheck/client.go b/go/internal/gitlabnet/healthcheck/client.go deleted file mode 100644 index 288b150..0000000 --- a/go/internal/gitlabnet/healthcheck/client.go +++ /dev/null @@ -1,54 +0,0 @@ -package healthcheck - -import ( - "fmt" - "net/http" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" -) - -const ( - checkPath = "/check" -) - -type Client struct { - config *config.Config - client *gitlabnet.GitlabClient -} - -type Response struct { - APIVersion string `json:"api_version"` - GitlabVersion string `json:"gitlab_version"` - GitlabRevision string `json:"gitlab_rev"` - Redis bool `json:"redis"` -} - -func NewClient(config *config.Config) (*Client, error) { - client, err := gitlabnet.GetClient(config) - if err != nil { - return nil, fmt.Errorf("Error creating http client: %v", err) - } - - return &Client{config: config, client: client}, nil -} - -func (c *Client) Check() (*Response, error) { - resp, err := c.client.Get(checkPath) - if err != nil { - return nil, err - } - - defer resp.Body.Close() - - return parse(resp) -} - -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/healthcheck/client_test.go b/go/internal/gitlabnet/healthcheck/client_test.go deleted file mode 100644 index d32e6f4..0000000 --- a/go/internal/gitlabnet/healthcheck/client_test.go +++ /dev/null @@ -1,48 +0,0 @@ -package healthcheck - -import ( - "encoding/json" - "net/http" - "testing" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" - - "github.com/stretchr/testify/require" -) - -var ( - requests = []testserver.TestRequestHandler{ - { - Path: "/api/v4/internal/check", - Handler: func(w http.ResponseWriter, r *http.Request) { - json.NewEncoder(w).Encode(testResponse) - }, - }, - } - - testResponse = &Response{ - APIVersion: "v4", - GitlabVersion: "v12.0.0-ee", - GitlabRevision: "3b13818e8330f68625d80d9bf5d8049c41fbe197", - Redis: true, - } -) - -func TestCheck(t *testing.T) { - client, cleanup := setup(t) - defer cleanup() - - result, err := client.Check() - require.NoError(t, err) - require.Equal(t, testResponse, result) -} - -func setup(t *testing.T) (*Client, func()) { - url, cleanup := testserver.StartSocketHttpServer(t, requests) - - client, err := NewClient(&config.Config{GitlabUrl: url}) - require.NoError(t, err) - - return client, cleanup -} diff --git a/go/internal/gitlabnet/httpclient_test.go b/go/internal/gitlabnet/httpclient_test.go deleted file mode 100644 index 9b635bd..0000000 --- a/go/internal/gitlabnet/httpclient_test.go +++ /dev/null @@ -1,96 +0,0 @@ -package gitlabnet - -import ( - "encoding/base64" - "fmt" - "io/ioutil" - "net/http" - "strings" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" -) - -const ( - username = "basic_auth_user" - password = "basic_auth_password" -) - -func TestBasicAuthSettings(t *testing.T) { - requests := []testserver.TestRequestHandler{ - { - Path: "/api/v4/internal/get_endpoint", - Handler: func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - - fmt.Fprint(w, r.Header.Get("Authorization")) - }, - }, - { - Path: "/api/v4/internal/post_endpoint", - Handler: func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodPost, r.Method) - - fmt.Fprint(w, r.Header.Get("Authorization")) - }, - }, - } - config := &config.Config{HttpSettings: config.HttpSettingsConfig{User: username, Password: password}} - - client, cleanup := setup(t, config, requests) - defer cleanup() - - response, err := client.Get("/get_endpoint") - require.NoError(t, err) - testBasicAuthHeaders(t, response) - - response, err = client.Post("/post_endpoint", nil) - require.NoError(t, err) - testBasicAuthHeaders(t, response) -} - -func testBasicAuthHeaders(t *testing.T, response *http.Response) { - defer response.Body.Close() - - require.NotNil(t, response) - responseBody, err := ioutil.ReadAll(response.Body) - assert.NoError(t, err) - - headerParts := strings.Split(string(responseBody), " ") - assert.Equal(t, "Basic", headerParts[0]) - - credentials, err := base64.StdEncoding.DecodeString(headerParts[1]) - require.NoError(t, err) - - assert.Equal(t, username+":"+password, string(credentials)) -} - -func TestEmptyBasicAuthSettings(t *testing.T) { - requests := []testserver.TestRequestHandler{ - { - Path: "/api/v4/internal/empty_basic_auth", - Handler: func(w http.ResponseWriter, r *http.Request) { - assert.Equal(t, "", r.Header.Get("Authorization")) - }, - }, - } - - client, cleanup := setup(t, &config.Config{}, requests) - defer cleanup() - - _, err := client.Get("/empty_basic_auth") - require.NoError(t, err) -} - -func setup(t *testing.T, config *config.Config, requests []testserver.TestRequestHandler) (*GitlabClient, func()) { - url, cleanup := testserver.StartHttpServer(t, requests) - - config.GitlabUrl = url - client, err := GetClient(config) - require.NoError(t, err) - - return client, cleanup -} diff --git a/go/internal/gitlabnet/httpsclient_test.go b/go/internal/gitlabnet/httpsclient_test.go deleted file mode 100644 index 04901df..0000000 --- a/go/internal/gitlabnet/httpsclient_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package gitlabnet - -import ( - "fmt" - "io/ioutil" - "net/http" - "path" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" -) - -func TestSuccessfulRequests(t *testing.T) { - testCases := []struct { - desc string - config *config.Config - }{ - { - desc: "Valid CaFile", - config: &config.Config{ - HttpSettings: config.HttpSettingsConfig{CaFile: path.Join(testhelper.TestRoot, "certs/valid/server.crt")}, - }, - }, - { - desc: "Valid CaPath", - config: &config.Config{ - HttpSettings: config.HttpSettingsConfig{CaPath: path.Join(testhelper.TestRoot, "certs/valid")}, - }, - }, - { - desc: "Self signed cert option enabled", - config: &config.Config{ - HttpSettings: config.HttpSettingsConfig{SelfSignedCert: true}, - }, - }, - { - desc: "Invalid cert with self signed cert option enabled", - config: &config.Config{ - HttpSettings: config.HttpSettingsConfig{SelfSignedCert: true, CaFile: path.Join(testhelper.TestRoot, "certs/valid/server.crt")}, - }, - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - client, cleanup := setupWithRequests(t, tc.config) - defer cleanup() - - response, err := client.Get("/hello") - require.NoError(t, err) - require.NotNil(t, response) - - defer response.Body.Close() - - responseBody, err := ioutil.ReadAll(response.Body) - assert.NoError(t, err) - assert.Equal(t, string(responseBody), "Hello") - }) - } -} - -func TestFailedRequests(t *testing.T) { - testCases := []struct { - desc string - config *config.Config - }{ - { - desc: "Invalid CaFile", - config: &config.Config{ - HttpSettings: config.HttpSettingsConfig{CaFile: path.Join(testhelper.TestRoot, "certs/invalid/server.crt")}, - }, - }, - { - desc: "Invalid CaPath", - config: &config.Config{ - HttpSettings: config.HttpSettingsConfig{CaPath: path.Join(testhelper.TestRoot, "certs/invalid")}, - }, - }, - { - desc: "Empty config", - config: &config.Config{}, - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - client, cleanup := setupWithRequests(t, tc.config) - defer cleanup() - - _, err := client.Get("/hello") - require.Error(t, err) - - assert.Equal(t, err.Error(), "Internal API unreachable") - }) - } -} - -func setupWithRequests(t *testing.T, config *config.Config) (*GitlabClient, func()) { - testDirCleanup, err := testhelper.PrepareTestRootDir() - require.NoError(t, err) - defer testDirCleanup() - - requests := []testserver.TestRequestHandler{ - { - Path: "/api/v4/internal/hello", - Handler: func(w http.ResponseWriter, r *http.Request) { - require.Equal(t, http.MethodGet, r.Method) - - fmt.Fprint(w, "Hello") - }, - }, - } - - url, cleanup := testserver.StartHttpsServer(t, requests) - - config.GitlabUrl = url - client, err := GetClient(config) - require.NoError(t, err) - - return client, cleanup -} diff --git a/go/internal/gitlabnet/lfsauthenticate/client.go b/go/internal/gitlabnet/lfsauthenticate/client.go deleted file mode 100644 index 51cb7a4..0000000 --- a/go/internal/gitlabnet/lfsauthenticate/client.go +++ /dev/null @@ -1,66 +0,0 @@ -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.Shell -} - -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.Shell) (*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 deleted file mode 100644 index 07484a7..0000000 --- a/go/internal/gitlabnet/lfsauthenticate/client_test.go +++ /dev/null @@ -1,117 +0,0 @@ -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.Shell - expectedOutput string - }{ - { - desc: "With bad response", - args: &commandargs.Shell{GitlabKeyId: "-1", CommandType: commandargs.UploadPack}, - expectedOutput: "Parsing failed", - }, - { - desc: "With API returns an error", - args: &commandargs.Shell{GitlabKeyId: "forbidden", CommandType: commandargs.UploadPack}, - expectedOutput: "Internal API error (403)", - }, - { - desc: "With API fails", - args: &commandargs.Shell{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.Shell{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) -} diff --git a/go/internal/gitlabnet/testserver/gitalyserver.go b/go/internal/gitlabnet/testserver/gitalyserver.go deleted file mode 100644 index 694fd41..0000000 --- a/go/internal/gitlabnet/testserver/gitalyserver.go +++ /dev/null @@ -1,78 +0,0 @@ -package testserver - -import ( - "io/ioutil" - "net" - "os" - "path" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - "google.golang.org/grpc" - - pb "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb" -) - -type testGitalyServer struct{} - -func (s *testGitalyServer) SSHReceivePack(stream pb.SSHService_SSHReceivePackServer) error { - req, err := stream.Recv() - if err != nil { - return err - } - - response := []byte("ReceivePack: " + req.GlId + " " + req.Repository.GlRepository) - stream.Send(&pb.SSHReceivePackResponse{Stdout: response}) - - return nil -} - -func (s *testGitalyServer) SSHUploadPack(stream pb.SSHService_SSHUploadPackServer) error { - req, err := stream.Recv() - if err != nil { - return err - } - - response := []byte("UploadPack: " + req.Repository.GlRepository) - stream.Send(&pb.SSHUploadPackResponse{Stdout: response}) - - return nil -} - -func (s *testGitalyServer) SSHUploadArchive(stream pb.SSHService_SSHUploadArchiveServer) error { - req, err := stream.Recv() - if err != nil { - return err - } - - response := []byte("UploadArchive: " + req.Repository.GlRepository) - stream.Send(&pb.SSHUploadArchiveResponse{Stdout: response}) - - return nil -} - -func StartGitalyServer(t *testing.T) (string, func()) { - tempDir, _ := ioutil.TempDir("", "gitlab-shell-test-api") - gitalySocketPath := path.Join(tempDir, "gitaly.sock") - - err := os.MkdirAll(filepath.Dir(gitalySocketPath), 0700) - require.NoError(t, err) - - server := grpc.NewServer() - - listener, err := net.Listen("unix", gitalySocketPath) - require.NoError(t, err) - - pb.RegisterSSHServiceServer(server, &testGitalyServer{}) - - go server.Serve(listener) - - gitalySocketUrl := "unix:" + gitalySocketPath - cleanup := func() { - server.Stop() - os.RemoveAll(tempDir) - } - - return gitalySocketUrl, cleanup -} diff --git a/go/internal/gitlabnet/testserver/testserver.go b/go/internal/gitlabnet/testserver/testserver.go deleted file mode 100644 index bf59ce4..0000000 --- a/go/internal/gitlabnet/testserver/testserver.go +++ /dev/null @@ -1,82 +0,0 @@ -package testserver - -import ( - "crypto/tls" - "io/ioutil" - "log" - "net" - "net/http" - "net/http/httptest" - "os" - "path" - "path/filepath" - "testing" - - "github.com/stretchr/testify/require" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" -) - -var ( - tempDir, _ = ioutil.TempDir("", "gitlab-shell-test-api") - testSocket = path.Join(tempDir, "internal.sock") -) - -type TestRequestHandler struct { - Path string - Handler func(w http.ResponseWriter, r *http.Request) -} - -func StartSocketHttpServer(t *testing.T, handlers []TestRequestHandler) (string, func()) { - err := os.MkdirAll(filepath.Dir(testSocket), 0700) - require.NoError(t, err) - - socketListener, err := net.Listen("unix", testSocket) - require.NoError(t, err) - - server := http.Server{ - Handler: buildHandler(handlers), - // We'll put this server through some nasty stuff we don't want - // in our test output - ErrorLog: log.New(ioutil.Discard, "", 0), - } - go server.Serve(socketListener) - - url := "http+unix://" + testSocket - - return url, cleanupSocket -} - -func StartHttpServer(t *testing.T, handlers []TestRequestHandler) (string, func()) { - server := httptest.NewServer(buildHandler(handlers)) - - return server.URL, server.Close -} - -func StartHttpsServer(t *testing.T, handlers []TestRequestHandler) (string, func()) { - crt := path.Join(testhelper.TestRoot, "certs/valid/server.crt") - key := path.Join(testhelper.TestRoot, "certs/valid/server.key") - - server := httptest.NewUnstartedServer(buildHandler(handlers)) - cer, err := tls.LoadX509KeyPair(crt, key) - require.NoError(t, err) - - server.TLS = &tls.Config{Certificates: []tls.Certificate{cer}} - server.StartTLS() - - return server.URL, server.Close -} - -func cleanupSocket() { - os.RemoveAll(tempDir) -} - -func buildHandler(handlers []TestRequestHandler) http.Handler { - h := http.NewServeMux() - - for _, handler := range handlers { - h.HandleFunc(handler.Path, handler.Handler) - } - - return h -} diff --git a/go/internal/gitlabnet/twofactorrecover/client.go b/go/internal/gitlabnet/twofactorrecover/client.go deleted file mode 100644 index 37067db..0000000 --- a/go/internal/gitlabnet/twofactorrecover/client.go +++ /dev/null @@ -1,89 +0,0 @@ -package twofactorrecover - -import ( - "errors" - "fmt" - "net/http" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/discover" -) - -type Client struct { - config *config.Config - client *gitlabnet.GitlabClient -} - -type Response struct { - Success bool `json:"success"` - RecoveryCodes []string `json:"recovery_codes"` - Message string `json:"message"` -} - -type RequestBody struct { - KeyId string `json:"key_id,omitempty"` - UserId int64 `json:"user_id,omitempty"` -} - -func NewClient(config *config.Config) (*Client, error) { - client, err := gitlabnet.GetClient(config) - if err != nil { - return nil, fmt.Errorf("Error creating http client: %v", err) - } - - return &Client{config: config, client: client}, nil -} - -func (c *Client) GetRecoveryCodes(args *commandargs.Shell) ([]string, error) { - requestBody, err := c.getRequestBody(args) - - if err != nil { - return nil, err - } - - response, err := c.client.Post("/two_factor_recovery_codes", requestBody) - if err != nil { - return nil, err - } - defer response.Body.Close() - - return parse(response) -} - -func parse(hr *http.Response) ([]string, error) { - response := &Response{} - if err := gitlabnet.ParseJSON(hr, response); err != nil { - return nil, err - } - - if !response.Success { - return nil, errors.New(response.Message) - } - - return response.RecoveryCodes, nil -} - -func (c *Client) getRequestBody(args *commandargs.Shell) (*RequestBody, error) { - client, err := discover.NewClient(c.config) - - if err != nil { - return nil, err - } - - var requestBody *RequestBody - if args.GitlabKeyId != "" { - requestBody = &RequestBody{KeyId: args.GitlabKeyId} - } else { - userInfo, err := client.GetByCommandArgs(args) - - if err != nil { - return nil, err - } - - requestBody = &RequestBody{UserId: userInfo.UserId} - } - - return requestBody, nil -} diff --git a/go/internal/gitlabnet/twofactorrecover/client_test.go b/go/internal/gitlabnet/twofactorrecover/client_test.go deleted file mode 100644 index a560fb1..0000000 --- a/go/internal/gitlabnet/twofactorrecover/client_test.go +++ /dev/null @@ -1,158 +0,0 @@ -package twofactorrecover - -import ( - "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/config" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/discover" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" -) - -var ( - requests []testserver.TestRequestHandler -) - -func initialize(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 *RequestBody - json.Unmarshal(b, &requestBody) - - switch requestBody.KeyId { - case "0": - body := map[string]interface{}{ - "success": true, - "recovery_codes": [2]string{"recovery 1", "codes 1"}, - } - json.NewEncoder(w).Encode(body) - case "1": - body := map[string]interface{}{ - "success": false, - "message": "missing user", - } - json.NewEncoder(w).Encode(body) - case "2": - w.WriteHeader(http.StatusForbidden) - body := &gitlabnet.ErrorResponse{ - Message: "Not allowed!", - } - json.NewEncoder(w).Encode(body) - case "3": - w.Write([]byte("{ \"message\": \"broken json!\"")) - case "4": - w.WriteHeader(http.StatusForbidden) - } - - if requestBody.UserId == 1 { - body := map[string]interface{}{ - "success": true, - "recovery_codes": [2]string{"recovery 2", "codes 2"}, - } - json.NewEncoder(w).Encode(body) - } - }, - }, - { - Path: "/api/v4/internal/discover", - Handler: func(w http.ResponseWriter, r *http.Request) { - body := &discover.Response{ - UserId: 1, - Username: "jane-doe", - Name: "Jane Doe", - } - json.NewEncoder(w).Encode(body) - }, - }, - } -} - -func TestGetRecoveryCodesByKeyId(t *testing.T) { - client, cleanup := setup(t) - defer cleanup() - - args := &commandargs.Shell{GitlabKeyId: "0"} - result, err := client.GetRecoveryCodes(args) - assert.NoError(t, err) - assert.Equal(t, []string{"recovery 1", "codes 1"}, result) -} - -func TestGetRecoveryCodesByUsername(t *testing.T) { - client, cleanup := setup(t) - defer cleanup() - - args := &commandargs.Shell{GitlabUsername: "jane-doe"} - result, err := client.GetRecoveryCodes(args) - assert.NoError(t, err) - assert.Equal(t, []string{"recovery 2", "codes 2"}, result) -} - -func TestMissingUser(t *testing.T) { - client, cleanup := setup(t) - defer cleanup() - - args := &commandargs.Shell{GitlabKeyId: "1"} - _, err := client.GetRecoveryCodes(args) - assert.Equal(t, "missing user", err.Error()) -} - -func TestErrorResponses(t *testing.T) { - client, cleanup := setup(t) - defer cleanup() - - testCases := []struct { - desc string - fakeId string - expectedError string - }{ - { - desc: "A response with an error message", - fakeId: "2", - expectedError: "Not allowed!", - }, - { - desc: "A response with bad JSON", - fakeId: "3", - expectedError: "Parsing failed", - }, - { - desc: "An error response without message", - fakeId: "4", - expectedError: "Internal API error (403)", - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - args := &commandargs.Shell{GitlabKeyId: tc.fakeId} - resp, err := client.GetRecoveryCodes(args) - - assert.EqualError(t, err, tc.expectedError) - assert.Nil(t, resp) - }) - } -} - -func setup(t *testing.T) (*Client, func()) { - initialize(t) - url, cleanup := testserver.StartSocketHttpServer(t, requests) - - client, err := NewClient(&config.Config{GitlabUrl: url}) - require.NoError(t, err) - - return client, cleanup -} diff --git a/go/internal/handler/exec.go b/go/internal/handler/exec.go deleted file mode 100644 index 1f3177d..0000000 --- a/go/internal/handler/exec.go +++ /dev/null @@ -1,96 +0,0 @@ -package handler - -import ( - "context" - "fmt" - "os" - - "gitlab.com/gitlab-org/gitaly/auth" - "gitlab.com/gitlab-org/gitaly/client" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" - "gitlab.com/gitlab-org/labkit/tracing" - "google.golang.org/grpc" -) - -// GitalyHandlerFunc implementations are responsible for making -// an appropriate Gitaly call using the provided client and context -// and returning an error from the Gitaly call. -type GitalyHandlerFunc func(ctx context.Context, client *grpc.ClientConn) (int32, error) - -type GitalyConn struct { - ctx context.Context - conn *grpc.ClientConn - close func() -} - -type GitalyCommand struct { - Config *config.Config - ServiceName string - Address string - Token string -} - -// RunGitalyCommand provides a bootstrap for Gitaly commands executed -// through GitLab-Shell. It ensures that logging, tracing and other -// common concerns are configured before executing the `handler`. -func (gc *GitalyCommand) RunGitalyCommand(handler GitalyHandlerFunc) error { - gitalyConn, err := getConn(gc) - - if err != nil { - return err - } - - _, err = handler(gitalyConn.ctx, gitalyConn.conn) - - gitalyConn.close() - - return err -} - -func getConn(gc *GitalyCommand) (*GitalyConn, error) { - if gc.Address == "" { - return nil, fmt.Errorf("no gitaly_address given") - } - - connOpts := client.DefaultDialOpts - if gc.Token != "" { - connOpts = append(client.DefaultDialOpts, grpc.WithPerRPCCredentials(gitalyauth.RPCCredentialsV2(gc.Token))) - } - - // Use a working directory that won't get removed or unmounted. - if err := os.Chdir("/"); err != nil { - return nil, err - } - - // Configure distributed tracing - serviceName := fmt.Sprintf("gitlab-shell-%v", gc.ServiceName) - closer := tracing.Initialize( - tracing.WithServiceName(serviceName), - - // For GitLab-Shell, we explicitly initialize tracing from a config file - // instead of the default environment variable (using GITLAB_TRACING) - // This decision was made owing to the difficulty in passing environment - // variables into GitLab-Shell processes. - // - // Processes are spawned as children of the SSH daemon, which tightly - // controls environment variables; doing this means we don't have to - // enable PermitUserEnvironment - tracing.WithConnectionString(gc.Config.GitlabTracing), - ) - - ctx, finished := tracing.ExtractFromEnv(context.Background()) - - conn, err := client.Dial(gc.Address, connOpts) - if err != nil { - return nil, err - } - - finish := func() { - finished() - closer.Close() - conn.Close() - } - - return &GitalyConn{ctx: ctx, conn: conn, close: finish}, nil -} diff --git a/go/internal/handler/exec_test.go b/go/internal/handler/exec_test.go deleted file mode 100644 index e6608ac..0000000 --- a/go/internal/handler/exec_test.go +++ /dev/null @@ -1,42 +0,0 @@ -package handler - -import ( - "context" - "errors" - "testing" - - "github.com/stretchr/testify/require" - "google.golang.org/grpc" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" -) - -func makeHandler(t *testing.T, err error) func(context.Context, *grpc.ClientConn) (int32, error) { - return func(ctx context.Context, client *grpc.ClientConn) (int32, error) { - require.NotNil(t, ctx) - require.NotNil(t, client) - - return 0, err - } -} - -func TestRunGitalyCommand(t *testing.T) { - cmd := GitalyCommand{ - Config: &config.Config{}, - Address: "tcp://localhost:9999", - } - - err := cmd.RunGitalyCommand(makeHandler(t, nil)) - require.NoError(t, err) - - expectedErr := errors.New("error") - err = cmd.RunGitalyCommand(makeHandler(t, expectedErr)) - require.Equal(t, err, expectedErr) -} - -func TestMissingGitalyAddress(t *testing.T) { - cmd := GitalyCommand{Config: &config.Config{}} - - err := cmd.RunGitalyCommand(makeHandler(t, nil)) - require.EqualError(t, err, "no gitaly_address given") -} diff --git a/go/internal/keyline/key_line.go b/go/internal/keyline/key_line.go deleted file mode 100644 index f92f50b..0000000 --- a/go/internal/keyline/key_line.go +++ /dev/null @@ -1,62 +0,0 @@ -package keyline - -import ( - "errors" - "fmt" - "path" - "regexp" - "strings" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/executable" -) - -var ( - keyRegex = regexp.MustCompile(`\A[a-z0-9-]+\z`) -) - -const ( - PublicKeyPrefix = "key" - PrincipalPrefix = "username" - SshOptions = "no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty" -) - -type KeyLine struct { - Id string // This can be either an ID of a Key or username - Value string // This can be either a public key or a principal name - Prefix string - RootDir string -} - -func NewPublicKeyLine(id string, publicKey string, rootDir string) (*KeyLine, error) { - return newKeyLine(id, publicKey, PublicKeyPrefix, rootDir) -} - -func NewPrincipalKeyLine(keyId string, principal string, rootDir string) (*KeyLine, error) { - return newKeyLine(keyId, principal, PrincipalPrefix, rootDir) -} - -func (k *KeyLine) ToString() string { - command := fmt.Sprintf("%s %s-%s", path.Join(k.RootDir, executable.BinDir, executable.GitlabShell), k.Prefix, k.Id) - - return fmt.Sprintf(`command="%s",%s %s`, command, SshOptions, k.Value) -} - -func newKeyLine(id string, value string, prefix string, rootDir string) (*KeyLine, error) { - if err := validate(id, value); err != nil { - return nil, err - } - - return &KeyLine{Id: id, Value: value, Prefix: prefix, RootDir: rootDir}, nil -} - -func validate(id string, value string) error { - if !keyRegex.MatchString(id) { - return errors.New(fmt.Sprintf("Invalid key_id: %s", id)) - } - - if strings.Contains(value, "\n") { - return errors.New(fmt.Sprintf("Invalid value: %s", value)) - } - - return nil -} diff --git a/go/internal/keyline/key_line_test.go b/go/internal/keyline/key_line_test.go deleted file mode 100644 index c6883c0..0000000 --- a/go/internal/keyline/key_line_test.go +++ /dev/null @@ -1,82 +0,0 @@ -package keyline - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func TestFailingNewPublicKeyLine(t *testing.T) { - testCases := []struct { - desc string - id string - publicKey string - expectedError string - }{ - { - desc: "When Id has non-alphanumeric and non-dash characters in it", - id: "key\n1", - publicKey: "public-key", - expectedError: "Invalid key_id: key\n1", - }, - { - desc: "When public key has newline in it", - id: "key", - publicKey: "public\nkey", - expectedError: "Invalid value: public\nkey", - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - result, err := NewPublicKeyLine(tc.id, tc.publicKey, "root-dir") - - require.Empty(t, result) - require.EqualError(t, err, tc.expectedError) - }) - } -} - -func TestFailingNewPrincipalKeyLine(t *testing.T) { - testCases := []struct { - desc string - keyId string - principal string - expectedError string - }{ - { - desc: "When username has non-alphanumeric and non-dash characters in it", - keyId: "username\n1", - principal: "principal", - expectedError: "Invalid key_id: username\n1", - }, - { - desc: "When principal has newline in it", - keyId: "username", - principal: "principal\n1", - expectedError: "Invalid value: principal\n1", - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - result, err := NewPrincipalKeyLine(tc.keyId, tc.principal, "root-dir") - - require.Empty(t, result) - require.EqualError(t, err, tc.expectedError) - }) - } -} - -func TestToString(t *testing.T) { - keyLine := &KeyLine{ - Id: "1", - Value: "public-key", - Prefix: "key", - RootDir: "/tmp", - } - - result := keyLine.ToString() - - require.Equal(t, `command="/tmp/bin/gitlab-shell key-1",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty public-key`, result) -} diff --git a/go/internal/logger/logger.go b/go/internal/logger/logger.go deleted file mode 100644 index c356d43..0000000 --- a/go/internal/logger/logger.go +++ /dev/null @@ -1,82 +0,0 @@ -package logger - -import ( - "fmt" - "io" - golog "log" - "log/syslog" - "os" - "sync" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" - - log "github.com/sirupsen/logrus" -) - -var ( - logWriter io.Writer - bootstrapLogger *golog.Logger - pid int - mutex sync.Mutex - ProgName string -) - -func Configure(cfg *config.Config) error { - mutex.Lock() - defer mutex.Unlock() - - pid = os.Getpid() - - var err error - logWriter, err = os.OpenFile(cfg.LogFile, os.O_WRONLY|os.O_APPEND, 0) - if err != nil { - return err - } - - log.SetOutput(logWriter) - if cfg.LogFormat == "json" { - log.SetFormatter(&log.JSONFormatter{}) - } - - return nil -} - -func logPrint(msg string, err error) { - mutex.Lock() - defer mutex.Unlock() - - if logWriter == nil { - bootstrapLogPrint(msg, err) - return - } - - log.WithError(err).WithFields(log.Fields{ - "pid": pid, - }).Error(msg) -} - -func Fatal(msg string, err error) { - logPrint(msg, err) - // We don't show the error to the end user because it can leak - // information that is private to the GitLab server. - fmt.Fprintf(os.Stderr, "%s: fatal: %s\n", ProgName, msg) - os.Exit(1) -} - -// If our log file is not available we want to log somewhere else, but -// not to standard error because that leaks information to the user. This -// function attemps to log to syslog. -// -// We assume the logging mutex is already locked. -func bootstrapLogPrint(msg string, err error) { - if bootstrapLogger == nil { - var err error - bootstrapLogger, err = syslog.NewLogger(syslog.LOG_ERR|syslog.LOG_USER, 0) - if err != nil { - // The message will not be logged. - return - } - } - - bootstrapLogger.Print(ProgName+":", msg+":", err) -} diff --git a/go/internal/sshenv/sshenv.go b/go/internal/sshenv/sshenv.go deleted file mode 100644 index 387feb2..0000000 --- a/go/internal/sshenv/sshenv.go +++ /dev/null @@ -1,15 +0,0 @@ -package sshenv - -import ( - "os" - "strings" -) - -func LocalAddr() string { - address := os.Getenv("SSH_CONNECTION") - - if address != "" { - return strings.Fields(address)[0] - } - return "" -} diff --git a/go/internal/sshenv/sshenv_test.go b/go/internal/sshenv/sshenv_test.go deleted file mode 100644 index d2207f5..0000000 --- a/go/internal/sshenv/sshenv_test.go +++ /dev/null @@ -1,20 +0,0 @@ -package sshenv - -import ( - "testing" - - "github.com/stretchr/testify/require" - "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" -) - -func TestLocalAddr(t *testing.T) { - cleanup, err := testhelper.Setenv("SSH_CONNECTION", "127.0.0.1 0") - require.NoError(t, err) - defer cleanup() - - require.Equal(t, LocalAddr(), "127.0.0.1") -} - -func TestEmptyLocalAddr(t *testing.T) { - require.Equal(t, LocalAddr(), "") -} diff --git a/go/internal/testhelper/requesthandlers/requesthandlers.go b/go/internal/testhelper/requesthandlers/requesthandlers.go deleted file mode 100644 index a7bc427..0000000 --- a/go/internal/testhelper/requesthandlers/requesthandlers.go +++ /dev/null @@ -1,58 +0,0 @@ -package requesthandlers - -import ( - "encoding/json" - "net/http" - "testing" - - "github.com/stretchr/testify/require" - - "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" -) - -func BuildDisallowedByApiHandlers(t *testing.T) []testserver.TestRequestHandler { - requests := []testserver.TestRequestHandler{ - { - Path: "/api/v4/internal/allowed", - Handler: func(w http.ResponseWriter, r *http.Request) { - body := map[string]interface{}{ - "status": false, - "message": "Disallowed by API call", - } - w.WriteHeader(http.StatusForbidden) - require.NoError(t, json.NewEncoder(w).Encode(body)) - }, - }, - } - - return requests -} - -func BuildAllowedWithGitalyHandlers(t *testing.T, gitalyAddress string) []testserver.TestRequestHandler { - requests := []testserver.TestRequestHandler{ - { - Path: "/api/v4/internal/allowed", - Handler: func(w http.ResponseWriter, r *http.Request) { - body := map[string]interface{}{ - "status": true, - "gl_id": "1", - "gitaly": map[string]interface{}{ - "repository": map[string]interface{}{ - "storage_name": "storage_name", - "relative_path": "relative_path", - "git_object_directory": "path/to/git_object_directory", - "git_alternate_object_directories": []string{"path/to/git_alternate_object_directory"}, - "gl_repository": "group/repo", - "gl_project_path": "group/project-path", - }, - "address": gitalyAddress, - "token": "token", - }, - } - require.NoError(t, json.NewEncoder(w).Encode(body)) - }, - }, - } - - return requests -} diff --git a/go/internal/testhelper/testdata/testroot/.gitlab_shell_secret b/go/internal/testhelper/testdata/testroot/.gitlab_shell_secret deleted file mode 100644 index 9bd459d..0000000 --- a/go/internal/testhelper/testdata/testroot/.gitlab_shell_secret +++ /dev/null @@ -1 +0,0 @@ -default-secret-content \ No newline at end of file diff --git a/go/internal/testhelper/testdata/testroot/certs/invalid/server.crt b/go/internal/testhelper/testdata/testroot/certs/invalid/server.crt deleted file mode 100644 index f8a42c1..0000000 --- a/go/internal/testhelper/testdata/testroot/certs/invalid/server.crt +++ /dev/null @@ -1,10 +0,0 @@ ------BEGIN CERTIFICATE----- -MinvalidcertAOvHjs6cs1R9MAoGCCqGSM49BAMCMBQxEjAQBgNVBAMMCWxvY2Fs -ainvalidcertOTA0MjQxNjM4NTBaFw0yOTA0MjExNjM4NTBaMBQxEjAQBgNVBAMM -CinvalidcertdDB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ5m7oW9OuL7aTAC04sL -3invalidcertdB2L0GsVCImav4PEpx6UAjkoiNGW9j0zPdNgxTYDjiCaGmr1aY2X -kinvalidcert7MNq7H8v7Ce/vrKkcDMOX8Gd/ddT3dEVqzAKBggqhkjOPQQDAgNp -AinvalidcertswcyjiB+A+ZjMSfaOsA2hAP0I3fkTcry386DePViMfnaIjm7rcuu -Jinvalidcert5V5CHypOxio1tOtGjaDkSH2FCdoatMyIe02+F6TIo44i4J/zjN52 -Jinvalidcert ------END CERTIFICATE----- diff --git a/go/internal/testhelper/testdata/testroot/certs/valid/dir/.gitkeep b/go/internal/testhelper/testdata/testroot/certs/valid/dir/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/go/internal/testhelper/testdata/testroot/certs/valid/server.crt b/go/internal/testhelper/testdata/testroot/certs/valid/server.crt deleted file mode 100644 index 11f1da7..0000000 --- a/go/internal/testhelper/testdata/testroot/certs/valid/server.crt +++ /dev/null @@ -1,22 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDrjCCApagAwIBAgIUHVNTmyz3p+7xSEMkSfhPz4BZfqwwDQYJKoZIhvcNAQEL -BQAwTjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcM -CVRoZSBDbG91ZDEWMBQGA1UECgwNTXkgQ29tcGFueSBDQTAeFw0xOTA5MjAxMDQ3 -NTlaFw0yOTA5MTcxMDQ3NTlaMF4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxp -Zm9ybmlhMRIwEAYDVQQHDAlUaGUgQ2xvdWQxDTALBgNVBAoMBERlbW8xFzAVBgNV -BAMMDk15IENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEAmte3G/eD+quamwyFl+2jEo8ngSAT0FWeY5ZAwRvdF4FgtTBLvbAdTnyi7pHM -esCSUkyxXHHPazM4SDV6uiu5LNKF0iz/NY76rLtFoqSGUgygTZHVbZ6NRXCNUZ0P -slD95wOCWvS9t9xgNXry66k8+mfZNhE+cFQfrO/pN5WpNuGyWTfKlUQw5NVL3mob -j3tSjI+wzSpbPMvbTQoBiZ/VHkyyc15YdrbePwFB2dJbxE/Xgsyk/TwWSUFnAs6i -1x2t+423NIm9rIDTdW2YYJJXv3MUcdDIxJnY0beGePMIymn9ZIRUJtK/ZXmwMb52 -v70+YTcsG67uSm31CR8jNt8qpQIDAQABo3QwcjAJBgNVHRMEAjAAMB0GA1UdDgQW -BBTxZ9SORmIwDs90TW8UXIVhDst4kjALBgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYI -KwYBBQUHAwIGCCsGAQUFBwMBMBoGA1UdEQQTMBGHBH8AAAGCCWxvY2FsaG9zdDAN -BgkqhkiG9w0BAQsFAAOCAQEAf4Iq94Su9TlkReMS4x2N5xZru9YoKQtrrxqWSRbp -oh5Lwtk9rJPy6q4IEPXzDsRI1YWCZe1Fw7zdiNfmoFRxjs59MBJ9YVrcFeyeAILg -LiAiRcGth2THpikCnLxmniGHUUX1WfjmcDEYMIs6BZ98N64VWwtuZqcJnJPmQs64 -lDrgW9oz6/8hPMeW58ok8PjkiG+E+srBaURoKwNe7vfPRVyq45N67/juH+4o6QBd -WP6ACjDM3RnxyWyW0S+sl3i3EAGgtwM6RIDhOG238HOIiA/I/+CCmITsvujz6jMN -bLdoPfnatZ7f5m9DuoOsGlYAZbLfOl2NywgO0jAlnHJGEQ== ------END CERTIFICATE----- diff --git a/go/internal/testhelper/testdata/testroot/certs/valid/server.key b/go/internal/testhelper/testdata/testroot/certs/valid/server.key deleted file mode 100644 index acec0fb..0000000 --- a/go/internal/testhelper/testdata/testroot/certs/valid/server.key +++ /dev/null @@ -1,27 +0,0 @@ ------BEGIN RSA PRIVATE KEY----- -MIIEogIBAAKCAQEAmte3G/eD+quamwyFl+2jEo8ngSAT0FWeY5ZAwRvdF4FgtTBL -vbAdTnyi7pHMesCSUkyxXHHPazM4SDV6uiu5LNKF0iz/NY76rLtFoqSGUgygTZHV -bZ6NRXCNUZ0PslD95wOCWvS9t9xgNXry66k8+mfZNhE+cFQfrO/pN5WpNuGyWTfK -lUQw5NVL3mobj3tSjI+wzSpbPMvbTQoBiZ/VHkyyc15YdrbePwFB2dJbxE/Xgsyk -/TwWSUFnAs6i1x2t+423NIm9rIDTdW2YYJJXv3MUcdDIxJnY0beGePMIymn9ZIRU -JtK/ZXmwMb52v70+YTcsG67uSm31CR8jNt8qpQIDAQABAoIBAEJQyNdtdlTRUfG9 -tymOWR0FuoGO322GfcNhAnKyIEqE2oo/GPEwkByhPJa4Ur7v4rrkpcFV7OOYmC40 -2U8KktAjibSuGM8zYSDBQ92YYP6a8bzHDIVaNl7bCWs+vQ49qcBavGWAFBC+jWXa -Nle/r6H/AAQr9nXdUYObbGKl8kbSUBNAqQHILsNyxQsAo12oqRnUWhIbfzUFBr1m -us93OsvpOYWgkbaBWk0brjp2X0eNGHctTboFxRknJcU6MQVL5degbgXhnCm4ir4O -E2KMubEwxePr5fPotWNQXCVin85OQv1eb70anfwoA2b5/ykb57jo5EDoiUoFsjLz -KLAaRQECgYEAzZNP/CpwCh5s31SDr7ajYfNIu8ie370g2Qbf4jrqVrOJ8Sj1LRYB -lS5+QbSRu4W6Ani3AQwZA09lS608G8w5rD7YGRVDCFuwJt+Yz5GcsSkso9B8DR4h -vCe2WuDutz7M5ikP1DAc/9x5HIzjQijxM1JJCNU2nR6QoFvV6wpVcpECgYEAwNK9 -oTqyb7UjNinAo9PFrFpnbX+DoGokGPsRyUwi9UkyRR0Uf7Kxjoq2C8zsCvnGdrE7 -kwUiWjyfAgMDF8+iWHYO1vD7m6NL31h/AAmo0NEQIBs0LFj0lF0xORzvXdTjhvuG -LxXhm927z4WBOCLTn8FAsBUjVBpmB6ffyZCVWNUCgYA3P4j2fz0/KvAdkSwW9CGy -uFxqwz8XaE/Eo9lVhnnmNTg0TMqfhFOGkUkzRWEJIaZc9a5RJLwwLI1Pqk4GNnul -c/pFu3YZb/LGb780wbB32FX77JL6P4fXdmDGyb6+Fq2giZaMcyXICauu5ZpJ9JDm -Nw4TxqF31ngN8MBr+4n9UQKBgAkxAoEQ/zh79fW6/8fPbHjOxmdd0LRw2s+mCC8E -RhZTKuZIgJWluvkEe7EMT6QmS+OUhzZ25DBQ+3NpGVilOSPmXMa6LgQ5QIChA0zJ -KRbrIE2nflEu3FnGJ3aFfpOGdmIU00yjSmHXrAA0aPh4EIZo++Bo4Yo8x+hNhElj -bvsRAoGADYZTUchbiVndk5QtnbwlDjrF5PmgjoDboBfv9/6FU+DzQRyOpl3kr0hs -OcZGE6xPZJidv1Bcv60L1VzTMj7spvMRTeumn2zEQGjkl6i/fSZzawjmKaKXKNkC -YfoV0RepB4TlNYGICaTcV+aKRIXivcpBGfduZEb39iUKCjh9Afg= ------END RSA PRIVATE KEY----- diff --git a/go/internal/testhelper/testdata/testroot/config.yml b/go/internal/testhelper/testdata/testroot/config.yml deleted file mode 100644 index e69de29..0000000 diff --git a/go/internal/testhelper/testdata/testroot/custom/my-contents-is-secret b/go/internal/testhelper/testdata/testroot/custom/my-contents-is-secret deleted file mode 100644 index 645b575..0000000 --- a/go/internal/testhelper/testdata/testroot/custom/my-contents-is-secret +++ /dev/null @@ -1 +0,0 @@ -custom-secret-content \ No newline at end of file diff --git a/go/internal/testhelper/testdata/testroot/gitlab-shell.log b/go/internal/testhelper/testdata/testroot/gitlab-shell.log deleted file mode 100644 index e69de29..0000000 diff --git a/go/internal/testhelper/testdata/testroot/responses/allowed.json b/go/internal/testhelper/testdata/testroot/responses/allowed.json deleted file mode 100644 index d0403d9..0000000 --- a/go/internal/testhelper/testdata/testroot/responses/allowed.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "status": true, - "gl_repository": "project-26", - "gl_project_path": "group/private", - "gl_id": "user-1", - "gl_username": "root", - "git_config_options": ["option"], - "gitaly": { - "repository": { - "storage_name": "default", - "relative_path": "@hashed/5f/9c/5f9c4ab08cac7457e9111a30e4664920607ea2c115a1433d7be98e97e64244ca.git", - "git_object_directory": "path/to/git_object_directory", - "git_alternate_object_directories": ["path/to/git_alternate_object_directory"], - "gl_repository": "project-26", - "gl_project_path": "group/private" - }, - "address": "unix:gitaly.socket", - "token": "token" - }, - "git_protocol": "protocol", - "gl_console_messages": ["console", "message"] -} diff --git a/go/internal/testhelper/testdata/testroot/responses/allowed_with_payload.json b/go/internal/testhelper/testdata/testroot/responses/allowed_with_payload.json deleted file mode 100644 index 331c3a9..0000000 --- a/go/internal/testhelper/testdata/testroot/responses/allowed_with_payload.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "status": true, - "gl_repository": "project-26", - "gl_project_path": "group/private", - "gl_id": "user-1", - "gl_username": "root", - "git_config_options": ["option"], - "gitaly": { - "repository": { - "storage_name": "default", - "relative_path": "@hashed/5f/9c/5f9c4ab08cac7457e9111a30e4664920607ea2c115a1433d7be98e97e64244ca.git", - "git_object_directory": "path/to/git_object_directory", - "git_alternate_object_directories": ["path/to/git_alternate_object_directory"], - "gl_repository": "project-26", - "gl_project_path": "group/private" - }, - "address": "unix:gitaly.socket", - "token": "token" - }, - "payload" : { - "action": "geo_proxy_to_primary", - "data": { - "api_endpoints": ["geo/proxy_git_push_ssh/info_refs", "geo/proxy_git_push_ssh/push"], - "gl_username": "custom", - "primary_repo": "https://repo/path", - "info_message": "message" - } - }, - "git_protocol": "protocol", - "gl_console_messages": ["console", "message"] -} diff --git a/go/internal/testhelper/testhelper.go b/go/internal/testhelper/testhelper.go deleted file mode 100644 index a925c79..0000000 --- a/go/internal/testhelper/testhelper.go +++ /dev/null @@ -1,93 +0,0 @@ -package testhelper - -import ( - "fmt" - "io/ioutil" - "os" - "path" - "runtime" - - "github.com/otiai10/copy" -) - -var ( - TestRoot, _ = ioutil.TempDir("", "test-gitlab-shell") -) - -func TempEnv(env map[string]string) func() { - var original = make(map[string]string) - for key, value := range env { - original[key] = os.Getenv(key) - os.Setenv(key, value) - } - - return func() { - for key, originalValue := range original { - os.Setenv(key, originalValue) - } - } -} - -func PrepareTestRootDir() (func(), error) { - if err := os.MkdirAll(TestRoot, 0700); err != nil { - return nil, err - } - - var oldWd string - cleanup := func() { - if oldWd != "" { - err := os.Chdir(oldWd) - if err != nil { - panic(err) - } - } - - if err := os.RemoveAll(TestRoot); err != nil { - panic(err) - } - } - - if err := copyTestData(); err != nil { - cleanup() - return nil, err - } - - oldWd, err := os.Getwd() - if err != nil { - cleanup() - return nil, err - } - - if err := os.Chdir(TestRoot); err != nil { - cleanup() - return nil, err - } - - return cleanup, nil -} - -func copyTestData() error { - testDataDir, err := getTestDataDir() - if err != nil { - return err - } - - testdata := path.Join(testDataDir, "testroot") - - return copy.Copy(testdata, TestRoot) -} - -func getTestDataDir() (string, error) { - _, currentFile, _, ok := runtime.Caller(0) - if !ok { - return "", fmt.Errorf("Could not get caller info") - } - - return path.Join(path.Dir(currentFile), "testdata"), nil -} - -func Setenv(key, value string) (func(), error) { - oldValue := os.Getenv(key) - err := os.Setenv(key, value) - return func() { os.Setenv(key, oldValue) }, err -} 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 ", 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 [...]", 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 ", + }, + { + 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 ", + }, + { + 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 [...]", + }, + { + 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\d+)\b`) + whoUsernameRegex = regexp.MustCompile(`\busername-(?P\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()) +} diff --git a/internal/config/config.go b/internal/config/config.go new file mode 100644 index 0000000..2231851 --- /dev/null +++ b/internal/config/config.go @@ -0,0 +1,123 @@ +package config + +import ( + "io/ioutil" + "net/url" + "os" + "path" + "path/filepath" + + yaml "gopkg.in/yaml.v2" +) + +const ( + configFile = "config.yml" + logFile = "gitlab-shell.log" + defaultSecretFileName = ".gitlab_shell_secret" +) + +type HttpSettingsConfig struct { + User string `yaml:"user"` + Password string `yaml:"password"` + ReadTimeoutSeconds uint64 `yaml:"read_timeout"` + CaFile string `yaml:"ca_file"` + CaPath string `yaml:"ca_path"` + SelfSignedCert bool `yaml:"self_signed_cert"` +} + +type Config struct { + RootDir string + LogFile string `yaml:"log_file"` + LogFormat string `yaml:"log_format"` + GitlabUrl string `yaml:"gitlab_url"` + GitlabTracing string `yaml:"gitlab_tracing"` + SecretFilePath string `yaml:"secret_file"` + Secret string `yaml:"secret"` + HttpSettings HttpSettingsConfig `yaml:"http_settings"` + HttpClient *HttpClient +} + +func New() (*Config, error) { + dir, err := os.Getwd() + if err != nil { + return nil, err + } + + return NewFromDir(dir) +} + +func NewFromDir(dir string) (*Config, error) { + return newFromFile(path.Join(dir, configFile)) +} + +func newFromFile(filename string) (*Config, error) { + cfg := &Config{RootDir: path.Dir(filename)} + + configBytes, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + if err := parseConfig(configBytes, cfg); err != nil { + return nil, err + } + + return cfg, nil +} + +// parseConfig expects YAML data in configBytes and a Config instance with RootDir set. +func parseConfig(configBytes []byte, cfg *Config) error { + if err := yaml.Unmarshal(configBytes, cfg); err != nil { + return err + } + + if cfg.LogFile == "" { + cfg.LogFile = logFile + } + + if len(cfg.LogFile) > 0 && cfg.LogFile[0] != '/' { + cfg.LogFile = path.Join(cfg.RootDir, cfg.LogFile) + } + + if cfg.LogFormat == "" { + cfg.LogFormat = "text" + } + + if cfg.GitlabUrl != "" { + unescapedUrl, err := url.PathUnescape(cfg.GitlabUrl) + if err != nil { + return err + } + + cfg.GitlabUrl = unescapedUrl + } + + if err := parseSecret(cfg); err != nil { + return err + } + + return nil +} + +func parseSecret(cfg *Config) error { + // The secret was parsed from yaml no need to read another file + if cfg.Secret != "" { + return nil + } + + if cfg.SecretFilePath == "" { + cfg.SecretFilePath = defaultSecretFileName + } + + if !filepath.IsAbs(cfg.SecretFilePath) { + cfg.SecretFilePath = path.Join(cfg.RootDir, cfg.SecretFilePath) + } + + secretFileContent, err := ioutil.ReadFile(cfg.SecretFilePath) + if err != nil { + return err + } + cfg.Secret = string(secretFileContent) + + return nil +} diff --git a/internal/config/config_test.go b/internal/config/config_test.go new file mode 100644 index 0000000..e31ff70 --- /dev/null +++ b/internal/config/config_test.go @@ -0,0 +1,112 @@ +package config + +import ( + "fmt" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" +) + +const ( + customSecret = "custom/my-contents-is-secret" +) + +var ( + testRoot = testhelper.TestRoot +) + +func TestParseConfig(t *testing.T) { + cleanup, err := testhelper.PrepareTestRootDir() + require.NoError(t, err) + defer cleanup() + + testCases := []struct { + yaml string + path string + format string + gitlabUrl string + secret string + httpSettings HttpSettingsConfig + }{ + { + path: path.Join(testRoot, "gitlab-shell.log"), + format: "text", + secret: "default-secret-content", + }, + { + yaml: "log_file: my-log.log", + path: path.Join(testRoot, "my-log.log"), + format: "text", + secret: "default-secret-content", + }, + { + yaml: "log_file: /qux/my-log.log", + path: "/qux/my-log.log", + format: "text", + secret: "default-secret-content", + }, + { + yaml: "log_format: json", + path: path.Join(testRoot, "gitlab-shell.log"), + format: "json", + secret: "default-secret-content", + }, + { + yaml: "gitlab_url: http+unix://%2Fpath%2Fto%2Fgitlab%2Fgitlab.socket", + path: path.Join(testRoot, "gitlab-shell.log"), + format: "text", + gitlabUrl: "http+unix:///path/to/gitlab/gitlab.socket", + secret: "default-secret-content", + }, + { + yaml: fmt.Sprintf("secret_file: %s", customSecret), + path: path.Join(testRoot, "gitlab-shell.log"), + format: "text", + secret: "custom-secret-content", + }, + { + yaml: fmt.Sprintf("secret_file: %s", path.Join(testRoot, customSecret)), + path: path.Join(testRoot, "gitlab-shell.log"), + format: "text", + secret: "custom-secret-content", + }, + { + yaml: "secret: an inline secret", + path: path.Join(testRoot, "gitlab-shell.log"), + format: "text", + secret: "an inline secret", + }, + { + yaml: "http_settings:\n user: user_basic_auth\n password: password_basic_auth\n read_timeout: 500", + path: path.Join(testRoot, "gitlab-shell.log"), + format: "text", + secret: "default-secret-content", + httpSettings: HttpSettingsConfig{User: "user_basic_auth", Password: "password_basic_auth", ReadTimeoutSeconds: 500}, + }, + { + yaml: "http_settings:\n ca_file: /etc/ssl/cert.pem\n ca_path: /etc/pki/tls/certs\n self_signed_cert: true", + path: path.Join(testRoot, "gitlab-shell.log"), + format: "text", + secret: "default-secret-content", + httpSettings: HttpSettingsConfig{CaFile: "/etc/ssl/cert.pem", CaPath: "/etc/pki/tls/certs", SelfSignedCert: true}, + }, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("yaml input: %q", tc.yaml), func(t *testing.T) { + cfg := Config{RootDir: testRoot} + + err := parseConfig([]byte(tc.yaml), &cfg) + require.NoError(t, err) + + assert.Equal(t, tc.path, cfg.LogFile) + assert.Equal(t, tc.format, cfg.LogFormat) + assert.Equal(t, tc.gitlabUrl, cfg.GitlabUrl) + assert.Equal(t, tc.secret, cfg.Secret) + assert.Equal(t, tc.httpSettings, cfg.HttpSettings) + }) + } +} diff --git a/internal/config/httpclient.go b/internal/config/httpclient.go new file mode 100644 index 0000000..c71efad --- /dev/null +++ b/internal/config/httpclient.go @@ -0,0 +1,122 @@ +package config + +import ( + "context" + "crypto/tls" + "crypto/x509" + "io/ioutil" + "net" + "net/http" + "path/filepath" + "strings" + "time" +) + +const ( + socketBaseUrl = "http://unix" + unixSocketProtocol = "http+unix://" + httpProtocol = "http://" + httpsProtocol = "https://" + defaultReadTimeoutSeconds = 300 +) + +type HttpClient struct { + HttpClient *http.Client + Host string +} + +func (c *Config) GetHttpClient() *HttpClient { + if c.HttpClient != nil { + return c.HttpClient + } + + var transport *http.Transport + var host string + if strings.HasPrefix(c.GitlabUrl, unixSocketProtocol) { + transport, host = c.buildSocketTransport() + } else if strings.HasPrefix(c.GitlabUrl, httpProtocol) { + transport, host = c.buildHttpTransport() + } else if strings.HasPrefix(c.GitlabUrl, httpsProtocol) { + transport, host = c.buildHttpsTransport() + } else { + return nil + } + + httpClient := &http.Client{ + Transport: transport, + Timeout: c.readTimeout(), + } + + client := &HttpClient{HttpClient: httpClient, Host: host} + + c.HttpClient = client + + return client +} + +func (c *Config) buildSocketTransport() (*http.Transport, string) { + socketPath := strings.TrimPrefix(c.GitlabUrl, unixSocketProtocol) + transport := &http.Transport{ + DialContext: func(ctx context.Context, _, _ string) (net.Conn, error) { + dialer := net.Dialer{} + return dialer.DialContext(ctx, "unix", socketPath) + }, + } + + return transport, socketBaseUrl +} + +func (c *Config) buildHttpsTransport() (*http.Transport, string) { + certPool, err := x509.SystemCertPool() + + if err != nil { + certPool = x509.NewCertPool() + } + + caFile := c.HttpSettings.CaFile + if caFile != "" { + addCertToPool(certPool, caFile) + } + + caPath := c.HttpSettings.CaPath + if caPath != "" { + fis, _ := ioutil.ReadDir(caPath) + for _, fi := range fis { + if fi.IsDir() { + continue + } + + addCertToPool(certPool, filepath.Join(caPath, fi.Name())) + } + } + + transport := &http.Transport{ + TLSClientConfig: &tls.Config{ + RootCAs: certPool, + InsecureSkipVerify: c.HttpSettings.SelfSignedCert, + }, + } + + return transport, c.GitlabUrl +} + +func addCertToPool(certPool *x509.CertPool, fileName string) { + cert, err := ioutil.ReadFile(fileName) + if err == nil { + certPool.AppendCertsFromPEM(cert) + } +} + +func (c *Config) buildHttpTransport() (*http.Transport, string) { + return &http.Transport{}, c.GitlabUrl +} + +func (c *Config) readTimeout() time.Duration { + timeoutSeconds := c.HttpSettings.ReadTimeoutSeconds + + if timeoutSeconds == 0 { + timeoutSeconds = defaultReadTimeoutSeconds + } + + return time.Duration(timeoutSeconds) * time.Second +} diff --git a/internal/config/httpclient_test.go b/internal/config/httpclient_test.go new file mode 100644 index 0000000..474deba --- /dev/null +++ b/internal/config/httpclient_test.go @@ -0,0 +1,22 @@ +package config + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestReadTimeout(t *testing.T) { + expectedSeconds := uint64(300) + + config := &Config{ + GitlabUrl: "http://localhost:3000", + HttpSettings: HttpSettingsConfig{ReadTimeoutSeconds: expectedSeconds}, + } + client := config.GetHttpClient() + + require.NotNil(t, client) + assert.Equal(t, time.Duration(expectedSeconds)*time.Second, client.HttpClient.Timeout) +} diff --git a/internal/executable/executable.go b/internal/executable/executable.go new file mode 100644 index 0000000..c6355b9 --- /dev/null +++ b/internal/executable/executable.go @@ -0,0 +1,60 @@ +package executable + +import ( + "os" + "path/filepath" +) + +const ( + BinDir = "bin" + Healthcheck = "check" + GitlabShell = "gitlab-shell" + AuthorizedKeysCheck = "gitlab-shell-authorized-keys-check" + AuthorizedPrincipalsCheck = "gitlab-shell-authorized-principals-check" +) + +type Executable struct { + Name string + RootDir string +} + +var ( + // osExecutable is overridden in tests + osExecutable = os.Executable +) + +func New(name string) (*Executable, error) { + path, err := osExecutable() + if err != nil { + return nil, err + } + + rootDir, err := findRootDir(path) + if err != nil { + return nil, err + } + + executable := &Executable{ + Name: name, + RootDir: rootDir, + } + + return executable, nil +} + +func findRootDir(path string) (string, error) { + // Start: /opt/.../gitlab-shell/bin/gitlab-shell + // Ends: /opt/.../gitlab-shell + rootDir := filepath.Dir(filepath.Dir(path)) + pathFromEnv := os.Getenv("GITLAB_SHELL_DIR") + + if pathFromEnv != "" { + if _, err := os.Stat(pathFromEnv); os.IsNotExist(err) { + return "", err + } + + rootDir = pathFromEnv + } + + return rootDir, nil +} diff --git a/internal/executable/executable_test.go b/internal/executable/executable_test.go new file mode 100644 index 0000000..581821d --- /dev/null +++ b/internal/executable/executable_test.go @@ -0,0 +1,104 @@ +package executable + +import ( + "errors" + "testing" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" + + "github.com/stretchr/testify/require" +) + +type fakeOs struct { + OldExecutable func() (string, error) + Path string + Error error +} + +func (f *fakeOs) Executable() (string, error) { + return f.Path, f.Error +} + +func (f *fakeOs) Setup() { + f.OldExecutable = osExecutable + osExecutable = f.Executable +} + +func (f *fakeOs) Cleanup() { + osExecutable = f.OldExecutable +} + +func TestNewSuccess(t *testing.T) { + testCases := []struct { + desc string + fakeOs *fakeOs + environment map[string]string + expectedRootDir string + }{ + { + desc: "GITLAB_SHELL_DIR env var is not defined", + fakeOs: &fakeOs{Path: "/tmp/bin/gitlab-shell"}, + expectedRootDir: "/tmp", + }, + { + desc: "GITLAB_SHELL_DIR env var is defined", + fakeOs: &fakeOs{Path: "/opt/bin/gitlab-shell"}, + environment: map[string]string{ + "GITLAB_SHELL_DIR": "/tmp", + }, + expectedRootDir: "/tmp", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + restoreEnv := testhelper.TempEnv(tc.environment) + defer restoreEnv() + + fake := tc.fakeOs + fake.Setup() + defer fake.Cleanup() + + result, err := New("gitlab-shell") + + require.NoError(t, err) + require.Equal(t, result.Name, "gitlab-shell") + require.Equal(t, result.RootDir, tc.expectedRootDir) + }) + } +} + +func TestNewFailure(t *testing.T) { + testCases := []struct { + desc string + fakeOs *fakeOs + environment map[string]string + }{ + { + desc: "failed to determine executable", + fakeOs: &fakeOs{Path: "", Error: errors.New("error")}, + }, + { + desc: "GITLAB_SHELL_DIR doesn't exist", + fakeOs: &fakeOs{Path: "/tmp/bin/gitlab-shell"}, + environment: map[string]string{ + "GITLAB_SHELL_DIR": "/tmp/non/existing/directory", + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + restoreEnv := testhelper.TempEnv(tc.environment) + defer restoreEnv() + + fake := tc.fakeOs + fake.Setup() + defer fake.Cleanup() + + _, err := New("gitlab-shell") + + require.Error(t, err) + }) + } +} diff --git a/internal/gitlabnet/accessverifier/client.go b/internal/gitlabnet/accessverifier/client.go new file mode 100644 index 0000000..eb67703 --- /dev/null +++ b/internal/gitlabnet/accessverifier/client.go @@ -0,0 +1,115 @@ +package accessverifier + +import ( + "fmt" + "net/http" + + 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/config" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/sshenv" +) + +const ( + protocol = "ssh" + anyChanges = "_any" +) + +type Client struct { + client *gitlabnet.GitlabClient +} + +type Request struct { + Action commandargs.CommandType `json:"action"` + Repo string `json:"project"` + Changes string `json:"changes"` + Protocol string `json:"protocol"` + KeyId string `json:"key_id,omitempty"` + Username string `json:"username,omitempty"` + CheckIp string `json:"check_ip,omitempty"` +} + +type Gitaly struct { + Repo pb.Repository `json:"repository"` + Address string `json:"address"` + Token string `json:"token"` +} + +type CustomPayloadData struct { + ApiEndpoints []string `json:"api_endpoints"` + Username string `json:"gl_username"` + PrimaryRepo string `json:"primary_repo"` + InfoMessage string `json:"info_message"` + UserId string `json:"gl_id,omitempty"` +} + +type CustomPayload struct { + Action string `json:"action"` + Data CustomPayloadData `json:"data"` +} + +type Response struct { + Success bool `json:"status"` + Message string `json:"message"` + Repo string `json:"gl_repository"` + UserId string `json:"gl_id"` + Username string `json:"gl_username"` + GitConfigOptions []string `json:"git_config_options"` + Gitaly Gitaly `json:"gitaly"` + GitProtocol string `json:"git_protocol"` + Payload CustomPayload `json:"payload"` + ConsoleMessages []string `json:"gl_console_messages"` + Who string + StatusCode int +} + +func NewClient(config *config.Config) (*Client, error) { + client, err := gitlabnet.GetClient(config) + if err != nil { + return nil, fmt.Errorf("Error creating http client: %v", err) + } + + return &Client{client: client}, nil +} + +func (c *Client) Verify(args *commandargs.Shell, action commandargs.CommandType, repo string) (*Response, error) { + request := &Request{Action: action, Repo: repo, Protocol: protocol, Changes: anyChanges} + + if args.GitlabUsername != "" { + request.Username = args.GitlabUsername + } else { + request.KeyId = args.GitlabKeyId + } + + request.CheckIp = sshenv.LocalAddr() + + response, err := c.client.Post("/allowed", request) + if err != nil { + return nil, err + } + defer response.Body.Close() + + return parse(response, args) +} + +func parse(hr *http.Response, args *commandargs.Shell) (*Response, error) { + response := &Response{} + if err := gitlabnet.ParseJSON(hr, response); err != nil { + return nil, err + } + + if args.GitlabKeyId != "" { + response.Who = "key-" + args.GitlabKeyId + } else { + response.Who = response.UserId + } + + response.StatusCode = hr.StatusCode + + return response, nil +} + +func (r *Response) IsCustomAction() bool { + return r.StatusCode == http.StatusMultipleChoices +} diff --git a/internal/gitlabnet/accessverifier/client_test.go b/internal/gitlabnet/accessverifier/client_test.go new file mode 100644 index 0000000..a4d1cb4 --- /dev/null +++ b/internal/gitlabnet/accessverifier/client_test.go @@ -0,0 +1,209 @@ +package accessverifier + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "path" + "testing" + + "github.com/stretchr/testify/require" + + 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/config" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" +) + +var ( + repo = "group/private" + action = commandargs.ReceivePack +) + +func buildExpectedResponse(who string) *Response { + response := &Response{ + Success: true, + UserId: "user-1", + Repo: "project-26", + Username: "root", + GitConfigOptions: []string{"option"}, + Gitaly: Gitaly{ + Repo: pb.Repository{ + StorageName: "default", + RelativePath: "@hashed/5f/9c/5f9c4ab08cac7457e9111a30e4664920607ea2c115a1433d7be98e97e64244ca.git", + GitObjectDirectory: "path/to/git_object_directory", + GitAlternateObjectDirectories: []string{"path/to/git_alternate_object_directory"}, + GlRepository: "project-26", + GlProjectPath: repo, + }, + Address: "unix:gitaly.socket", + Token: "token", + }, + GitProtocol: "protocol", + Payload: CustomPayload{}, + ConsoleMessages: []string{"console", "message"}, + Who: who, + StatusCode: 200, + } + + return response +} + +func TestSuccessfulResponses(t *testing.T) { + client, cleanup := setup(t) + defer cleanup() + + testCases := []struct { + desc string + args *commandargs.Shell + who string + }{ + { + desc: "Provide key id within the request", + args: &commandargs.Shell{GitlabKeyId: "1"}, + who: "key-1", + }, { + desc: "Provide username within the request", + args: &commandargs.Shell{GitlabUsername: "first"}, + who: "user-1", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + result, err := client.Verify(tc.args, action, repo) + require.NoError(t, err) + + response := buildExpectedResponse(tc.who) + require.Equal(t, response, result) + }) + } +} + +func TestGetCustomAction(t *testing.T) { + client, cleanup := setup(t) + defer cleanup() + + args := &commandargs.Shell{GitlabUsername: "custom"} + result, err := client.Verify(args, action, repo) + require.NoError(t, err) + + response := buildExpectedResponse("user-1") + response.Payload = CustomPayload{ + Action: "geo_proxy_to_primary", + Data: CustomPayloadData{ + ApiEndpoints: []string{"geo/proxy_git_push_ssh/info_refs", "geo/proxy_git_push_ssh/push"}, + Username: "custom", + PrimaryRepo: "https://repo/path", + InfoMessage: "message", + }, + } + response.StatusCode = 300 + + require.True(t, response.IsCustomAction()) + require.Equal(t, response, result) +} + +func TestErrorResponses(t *testing.T) { + client, cleanup := setup(t) + defer cleanup() + + testCases := []struct { + desc string + fakeId string + expectedError string + }{ + { + desc: "A response with an error message", + fakeId: "2", + expectedError: "Not allowed!", + }, + { + desc: "A response with bad JSON", + fakeId: "3", + expectedError: "Parsing failed", + }, + { + desc: "An error response without message", + fakeId: "4", + expectedError: "Internal API error (403)", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + args := &commandargs.Shell{GitlabKeyId: tc.fakeId} + resp, err := client.Verify(args, action, repo) + + require.EqualError(t, err, tc.expectedError) + require.Nil(t, resp) + }) + } +} + +func setup(t *testing.T) (*Client, func()) { + testDirCleanup, err := testhelper.PrepareTestRootDir() + require.NoError(t, err) + defer testDirCleanup() + + body, err := ioutil.ReadFile(path.Join(testhelper.TestRoot, "responses/allowed.json")) + require.NoError(t, err) + + allowedWithPayloadPath := path.Join(testhelper.TestRoot, "responses/allowed_with_payload.json") + bodyWithPayload, err := ioutil.ReadFile(allowedWithPayloadPath) + require.NoError(t, err) + + requests := []testserver.TestRequestHandler{ + { + Path: "/api/v4/internal/allowed", + Handler: func(w http.ResponseWriter, r *http.Request) { + b, err := ioutil.ReadAll(r.Body) + require.NoError(t, err) + + var requestBody *Request + require.NoError(t, json.Unmarshal(b, &requestBody)) + + switch requestBody.Username { + case "first": + _, err = w.Write(body) + require.NoError(t, err) + case "second": + errBody := map[string]interface{}{ + "status": false, + "message": "missing user", + } + require.NoError(t, json.NewEncoder(w).Encode(errBody)) + case "custom": + w.WriteHeader(http.StatusMultipleChoices) + _, err = w.Write(bodyWithPayload) + require.NoError(t, err) + } + + switch requestBody.KeyId { + case "1": + _, err = w.Write(body) + require.NoError(t, err) + case "2": + w.WriteHeader(http.StatusForbidden) + errBody := &gitlabnet.ErrorResponse{ + Message: "Not allowed!", + } + require.NoError(t, json.NewEncoder(w).Encode(errBody)) + case "3": + w.Write([]byte("{ \"message\": \"broken json!\"")) + case "4": + w.WriteHeader(http.StatusForbidden) + } + }, + }, + } + + url, cleanup := testserver.StartSocketHttpServer(t, requests) + + client, err := NewClient(&config.Config{GitlabUrl: url}) + require.NoError(t, err) + + return client, cleanup +} diff --git a/internal/gitlabnet/authorizedkeys/client.go b/internal/gitlabnet/authorizedkeys/client.go new file mode 100644 index 0000000..28b85fc --- /dev/null +++ b/internal/gitlabnet/authorizedkeys/client.go @@ -0,0 +1,65 @@ +package authorizedkeys + +import ( + "fmt" + "net/url" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" +) + +const ( + AuthorizedKeysPath = "/authorized_keys" +) + +type Client struct { + config *config.Config + client *gitlabnet.GitlabClient +} + +type Response struct { + Id int64 `json:"id"` + Key string `json:"key"` +} + +func NewClient(config *config.Config) (*Client, error) { + client, err := gitlabnet.GetClient(config) + if err != nil { + return nil, fmt.Errorf("Error creating http client: %v", err) + } + + return &Client{config: config, client: client}, nil +} + +func (c *Client) GetByKey(key string) (*Response, error) { + path, err := pathWithKey(key) + if err != nil { + return nil, err + } + + response, err := c.client.Get(path) + if err != nil { + return nil, err + } + defer response.Body.Close() + + parsedResponse := &Response{} + if err := gitlabnet.ParseJSON(response, parsedResponse); err != nil { + return nil, err + } + + return parsedResponse, nil +} + +func pathWithKey(key string) (string, error) { + u, err := url.Parse(AuthorizedKeysPath) + if err != nil { + return "", err + } + + params := u.Query() + params.Set("key", key) + u.RawQuery = params.Encode() + + return u.String(), nil +} diff --git a/internal/gitlabnet/authorizedkeys/client_test.go b/internal/gitlabnet/authorizedkeys/client_test.go new file mode 100644 index 0000000..c73aab2 --- /dev/null +++ b/internal/gitlabnet/authorizedkeys/client_test.go @@ -0,0 +1,105 @@ +package authorizedkeys + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" +) + +var ( + requests []testserver.TestRequestHandler +) + +func init() { + 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 := &Response{ + Id: 1, + Key: "public-key", + } + json.NewEncoder(w).Encode(body) + } else if r.URL.Query().Get("key") == "broken-message" { + w.WriteHeader(http.StatusForbidden) + body := &gitlabnet.ErrorResponse{ + Message: "Not allowed!", + } + json.NewEncoder(w).Encode(body) + } else if r.URL.Query().Get("key") == "broken-json" { + w.Write([]byte("{ \"message\": \"broken json!\"")) + } else if r.URL.Query().Get("key") == "broken-empty" { + w.WriteHeader(http.StatusForbidden) + } else { + w.WriteHeader(http.StatusNotFound) + } + }, + }, + } +} + +func TestGetByKey(t *testing.T) { + client, cleanup := setup(t) + defer cleanup() + + result, err := client.GetByKey("key") + require.NoError(t, err) + require.Equal(t, &Response{Id: 1, Key: "public-key"}, result) +} + +func TestGetByKeyErrorResponses(t *testing.T) { + client, cleanup := setup(t) + defer cleanup() + + testCases := []struct { + desc string + key string + expectedError string + }{ + { + desc: "A response with an error message", + key: "broken-message", + expectedError: "Not allowed!", + }, + { + desc: "A response with bad JSON", + key: "broken-json", + expectedError: "Parsing failed", + }, + { + desc: "A forbidden (403) response without message", + key: "broken-empty", + expectedError: "Internal API error (403)", + }, + { + desc: "A not found (404) response without message", + key: "not-found", + expectedError: "Internal API error (404)", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + resp, err := client.GetByKey(tc.key) + + require.EqualError(t, err, tc.expectedError) + require.Nil(t, resp) + }) + } +} + +func setup(t *testing.T) (*Client, func()) { + url, cleanup := testserver.StartSocketHttpServer(t, requests) + + client, err := NewClient(&config.Config{GitlabUrl: url}) + require.NoError(t, err) + + return client, cleanup +} diff --git a/internal/gitlabnet/client.go b/internal/gitlabnet/client.go new file mode 100644 index 0000000..dacb1d6 --- /dev/null +++ b/internal/gitlabnet/client.go @@ -0,0 +1,132 @@ +package gitlabnet + +import ( + "bytes" + "encoding/base64" + "encoding/json" + "fmt" + "io" + "net/http" + "strings" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" +) + +const ( + internalApiPath = "/api/v4/internal" + secretHeaderName = "Gitlab-Shared-Secret" +) + +var ( + ParsingError = fmt.Errorf("Parsing failed") +) + +type ErrorResponse struct { + Message string `json:"message"` +} + +type GitlabClient struct { + httpClient *http.Client + config *config.Config + host string +} + +func GetClient(config *config.Config) (*GitlabClient, error) { + client := config.GetHttpClient() + + if client == nil { + return nil, fmt.Errorf("Unsupported protocol") + } + + return &GitlabClient{httpClient: client.HttpClient, config: config, host: client.Host}, nil +} + +func normalizePath(path string) string { + if !strings.HasPrefix(path, "/") { + path = "/" + path + } + + if !strings.HasPrefix(path, internalApiPath) { + path = internalApiPath + path + } + return path +} + +func newRequest(method, host, path string, data interface{}) (*http.Request, error) { + var jsonReader io.Reader + if data != nil { + jsonData, err := json.Marshal(data) + if err != nil { + return nil, err + } + + jsonReader = bytes.NewReader(jsonData) + } + + request, err := http.NewRequest(method, host+path, jsonReader) + if err != nil { + return nil, err + } + + return request, nil +} + +func parseError(resp *http.Response) error { + if resp.StatusCode >= 200 && resp.StatusCode <= 399 { + return nil + } + defer resp.Body.Close() + parsedResponse := &ErrorResponse{} + + if err := json.NewDecoder(resp.Body).Decode(parsedResponse); err != nil { + return fmt.Errorf("Internal API error (%v)", resp.StatusCode) + } else { + return fmt.Errorf(parsedResponse.Message) + } + +} + +func (c *GitlabClient) Get(path string) (*http.Response, error) { + return c.DoRequest(http.MethodGet, normalizePath(path), nil) +} + +func (c *GitlabClient) Post(path string, data interface{}) (*http.Response, error) { + return c.DoRequest(http.MethodPost, normalizePath(path), data) +} + +func (c *GitlabClient) DoRequest(method, path string, data interface{}) (*http.Response, error) { + request, err := newRequest(method, c.host, path, data) + if err != nil { + return nil, err + } + + user, password := c.config.HttpSettings.User, c.config.HttpSettings.Password + if user != "" && password != "" { + request.SetBasicAuth(user, password) + } + + encodedSecret := base64.StdEncoding.EncodeToString([]byte(c.config.Secret)) + request.Header.Set(secretHeaderName, encodedSecret) + + request.Header.Add("Content-Type", "application/json") + request.Close = true + + response, err := c.httpClient.Do(request) + if err != nil { + return nil, fmt.Errorf("Internal API unreachable") + } + + if err := parseError(response); err != nil { + return nil, err + } + + return response, nil +} + +func ParseJSON(hr *http.Response, response interface{}) error { + if err := json.NewDecoder(hr.Body).Decode(response); err != nil { + return ParsingError + } + + return nil +} diff --git a/internal/gitlabnet/client_test.go b/internal/gitlabnet/client_test.go new file mode 100644 index 0000000..e8499dc --- /dev/null +++ b/internal/gitlabnet/client_test.go @@ -0,0 +1,219 @@ +package gitlabnet + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "io/ioutil" + "net/http" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" +) + +func TestClients(t *testing.T) { + testDirCleanup, err := testhelper.PrepareTestRootDir() + require.NoError(t, err) + defer testDirCleanup() + + requests := []testserver.TestRequestHandler{ + { + Path: "/api/v4/internal/hello", + Handler: func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + + fmt.Fprint(w, "Hello") + }, + }, + { + Path: "/api/v4/internal/post_endpoint", + Handler: func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + b, err := ioutil.ReadAll(r.Body) + defer r.Body.Close() + + require.NoError(t, err) + + fmt.Fprint(w, "Echo: "+string(b)) + }, + }, + { + Path: "/api/v4/internal/auth", + Handler: func(w http.ResponseWriter, r *http.Request) { + fmt.Fprint(w, r.Header.Get(secretHeaderName)) + }, + }, + { + Path: "/api/v4/internal/error", + Handler: func(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusBadRequest) + body := map[string]string{ + "message": "Don't do that", + } + json.NewEncoder(w).Encode(body) + }, + }, + { + Path: "/api/v4/internal/broken", + Handler: func(w http.ResponseWriter, r *http.Request) { + panic("Broken") + }, + }, + } + + testCases := []struct { + desc string + config *config.Config + server func(*testing.T, []testserver.TestRequestHandler) (string, func()) + }{ + { + desc: "Socket client", + config: &config.Config{}, + server: testserver.StartSocketHttpServer, + }, + { + desc: "Http client", + config: &config.Config{}, + server: testserver.StartHttpServer, + }, + { + desc: "Https client", + config: &config.Config{ + HttpSettings: config.HttpSettingsConfig{CaFile: path.Join(testhelper.TestRoot, "certs/valid/server.crt")}, + }, + server: testserver.StartHttpsServer, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + url, cleanup := tc.server(t, requests) + defer cleanup() + + tc.config.GitlabUrl = url + tc.config.Secret = "sssh, it's a secret" + + client, err := GetClient(tc.config) + require.NoError(t, err) + + testBrokenRequest(t, client) + testSuccessfulGet(t, client) + testSuccessfulPost(t, client) + testMissing(t, client) + testErrorMessage(t, client) + testAuthenticationHeader(t, client) + }) + } +} + +func testSuccessfulGet(t *testing.T, client *GitlabClient) { + t.Run("Successful get", func(t *testing.T) { + response, err := client.Get("/hello") + require.NoError(t, err) + require.NotNil(t, response) + + defer response.Body.Close() + + responseBody, err := ioutil.ReadAll(response.Body) + assert.NoError(t, err) + assert.Equal(t, string(responseBody), "Hello") + }) +} + +func testSuccessfulPost(t *testing.T, client *GitlabClient) { + t.Run("Successful Post", func(t *testing.T) { + data := map[string]string{"key": "value"} + + response, err := client.Post("/post_endpoint", data) + require.NoError(t, err) + require.NotNil(t, response) + + defer response.Body.Close() + + responseBody, err := ioutil.ReadAll(response.Body) + assert.NoError(t, err) + assert.Equal(t, "Echo: {\"key\":\"value\"}", string(responseBody)) + }) +} + +func testMissing(t *testing.T, client *GitlabClient) { + t.Run("Missing error for GET", func(t *testing.T) { + response, err := client.Get("/missing") + assert.EqualError(t, err, "Internal API error (404)") + assert.Nil(t, response) + }) + + t.Run("Missing error for POST", func(t *testing.T) { + response, err := client.Post("/missing", map[string]string{}) + assert.EqualError(t, err, "Internal API error (404)") + assert.Nil(t, response) + }) +} + +func testErrorMessage(t *testing.T, client *GitlabClient) { + t.Run("Error with message for GET", func(t *testing.T) { + response, err := client.Get("/error") + assert.EqualError(t, err, "Don't do that") + assert.Nil(t, response) + }) + + t.Run("Error with message for POST", func(t *testing.T) { + response, err := client.Post("/error", map[string]string{}) + assert.EqualError(t, err, "Don't do that") + assert.Nil(t, response) + }) +} + +func testBrokenRequest(t *testing.T, client *GitlabClient) { + t.Run("Broken request for GET", func(t *testing.T) { + response, err := client.Get("/broken") + assert.EqualError(t, err, "Internal API unreachable") + assert.Nil(t, response) + }) + + t.Run("Broken request for POST", func(t *testing.T) { + response, err := client.Post("/broken", map[string]string{}) + assert.EqualError(t, err, "Internal API unreachable") + assert.Nil(t, response) + }) +} + +func testAuthenticationHeader(t *testing.T, client *GitlabClient) { + t.Run("Authentication headers for GET", func(t *testing.T) { + response, err := client.Get("/auth") + require.NoError(t, err) + require.NotNil(t, response) + + defer response.Body.Close() + + responseBody, err := ioutil.ReadAll(response.Body) + require.NoError(t, err) + + header, err := base64.StdEncoding.DecodeString(string(responseBody)) + require.NoError(t, err) + assert.Equal(t, "sssh, it's a secret", string(header)) + }) + + t.Run("Authentication headers for POST", func(t *testing.T) { + response, err := client.Post("/auth", map[string]string{}) + require.NoError(t, err) + require.NotNil(t, response) + + defer response.Body.Close() + + responseBody, err := ioutil.ReadAll(response.Body) + require.NoError(t, err) + + header, err := base64.StdEncoding.DecodeString(string(responseBody)) + require.NoError(t, err) + assert.Equal(t, "sssh, it's a secret", string(header)) + }) +} diff --git a/internal/gitlabnet/discover/client.go b/internal/gitlabnet/discover/client.go new file mode 100644 index 0000000..46ab2de --- /dev/null +++ b/internal/gitlabnet/discover/client.go @@ -0,0 +1,71 @@ +package discover + +import ( + "fmt" + "net/http" + "net/url" + + "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 +} + +type Response struct { + UserId int64 `json:"id"` + Name string `json:"name"` + Username string `json:"username"` +} + +func NewClient(config *config.Config) (*Client, error) { + client, err := gitlabnet.GetClient(config) + if err != nil { + return nil, fmt.Errorf("Error creating http client: %v", err) + } + + return &Client{config: config, client: client}, nil +} + +func (c *Client) GetByCommandArgs(args *commandargs.Shell) (*Response, error) { + params := url.Values{} + if args.GitlabUsername != "" { + params.Add("username", args.GitlabUsername) + } else if args.GitlabKeyId != "" { + params.Add("key_id", args.GitlabKeyId) + } else { + // There was no 'who' information, this matches the ruby error + // message. + return nil, fmt.Errorf("who='' is invalid") + } + + return c.getResponse(params) +} + +func (c *Client) getResponse(params url.Values) (*Response, error) { + path := "/discover?" + params.Encode() + + response, err := c.client.Get(path) + 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 +} + +func (r *Response) IsAnonymous() bool { + return r.UserId < 1 +} diff --git a/internal/gitlabnet/discover/client_test.go b/internal/gitlabnet/discover/client_test.go new file mode 100644 index 0000000..b98a28e --- /dev/null +++ b/internal/gitlabnet/discover/client_test.go @@ -0,0 +1,137 @@ +package discover + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + "testing" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +var ( + requests []testserver.TestRequestHandler +) + +func init() { + requests = []testserver.TestRequestHandler{ + { + Path: "/api/v4/internal/discover", + Handler: func(w http.ResponseWriter, r *http.Request) { + if r.URL.Query().Get("key_id") == "1" { + body := &Response{ + UserId: 2, + Username: "alex-doe", + Name: "Alex Doe", + } + json.NewEncoder(w).Encode(body) + } else if r.URL.Query().Get("username") == "jane-doe" { + body := &Response{ + UserId: 1, + Username: "jane-doe", + Name: "Jane Doe", + } + json.NewEncoder(w).Encode(body) + } else if r.URL.Query().Get("username") == "broken_message" { + w.WriteHeader(http.StatusForbidden) + body := &gitlabnet.ErrorResponse{ + Message: "Not allowed!", + } + json.NewEncoder(w).Encode(body) + } else if r.URL.Query().Get("username") == "broken_json" { + w.Write([]byte("{ \"message\": \"broken json!\"")) + } else if r.URL.Query().Get("username") == "broken_empty" { + w.WriteHeader(http.StatusForbidden) + } else { + fmt.Fprint(w, "null") + } + }, + }, + } +} + +func TestGetByKeyId(t *testing.T) { + client, cleanup := setup(t) + defer cleanup() + + params := url.Values{} + params.Add("key_id", "1") + result, err := client.getResponse(params) + assert.NoError(t, err) + assert.Equal(t, &Response{UserId: 2, Username: "alex-doe", Name: "Alex Doe"}, result) +} + +func TestGetByUsername(t *testing.T) { + client, cleanup := setup(t) + defer cleanup() + + params := url.Values{} + params.Add("username", "jane-doe") + result, err := client.getResponse(params) + assert.NoError(t, err) + assert.Equal(t, &Response{UserId: 1, Username: "jane-doe", Name: "Jane Doe"}, result) +} + +func TestMissingUser(t *testing.T) { + client, cleanup := setup(t) + defer cleanup() + + params := url.Values{} + params.Add("username", "missing") + result, err := client.getResponse(params) + assert.NoError(t, err) + assert.True(t, result.IsAnonymous()) +} + +func TestErrorResponses(t *testing.T) { + client, cleanup := setup(t) + defer cleanup() + + testCases := []struct { + desc string + fakeUsername string + expectedError string + }{ + { + desc: "A response with an error message", + fakeUsername: "broken_message", + expectedError: "Not allowed!", + }, + { + desc: "A response with bad JSON", + fakeUsername: "broken_json", + expectedError: "Parsing failed", + }, + { + desc: "An error response without message", + fakeUsername: "broken_empty", + expectedError: "Internal API error (403)", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + params := url.Values{} + params.Add("username", tc.fakeUsername) + resp, err := client.getResponse(params) + + assert.EqualError(t, err, tc.expectedError) + assert.Nil(t, resp) + }) + } +} + +func setup(t *testing.T) (*Client, func()) { + url, cleanup := testserver.StartSocketHttpServer(t, requests) + + client, err := NewClient(&config.Config{GitlabUrl: url}) + require.NoError(t, err) + + return client, cleanup +} diff --git a/internal/gitlabnet/healthcheck/client.go b/internal/gitlabnet/healthcheck/client.go new file mode 100644 index 0000000..288b150 --- /dev/null +++ b/internal/gitlabnet/healthcheck/client.go @@ -0,0 +1,54 @@ +package healthcheck + +import ( + "fmt" + "net/http" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" +) + +const ( + checkPath = "/check" +) + +type Client struct { + config *config.Config + client *gitlabnet.GitlabClient +} + +type Response struct { + APIVersion string `json:"api_version"` + GitlabVersion string `json:"gitlab_version"` + GitlabRevision string `json:"gitlab_rev"` + Redis bool `json:"redis"` +} + +func NewClient(config *config.Config) (*Client, error) { + client, err := gitlabnet.GetClient(config) + if err != nil { + return nil, fmt.Errorf("Error creating http client: %v", err) + } + + return &Client{config: config, client: client}, nil +} + +func (c *Client) Check() (*Response, error) { + resp, err := c.client.Get(checkPath) + if err != nil { + return nil, err + } + + defer resp.Body.Close() + + return parse(resp) +} + +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/internal/gitlabnet/healthcheck/client_test.go b/internal/gitlabnet/healthcheck/client_test.go new file mode 100644 index 0000000..d32e6f4 --- /dev/null +++ b/internal/gitlabnet/healthcheck/client_test.go @@ -0,0 +1,48 @@ +package healthcheck + +import ( + "encoding/json" + "net/http" + "testing" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" + + "github.com/stretchr/testify/require" +) + +var ( + requests = []testserver.TestRequestHandler{ + { + Path: "/api/v4/internal/check", + Handler: func(w http.ResponseWriter, r *http.Request) { + json.NewEncoder(w).Encode(testResponse) + }, + }, + } + + testResponse = &Response{ + APIVersion: "v4", + GitlabVersion: "v12.0.0-ee", + GitlabRevision: "3b13818e8330f68625d80d9bf5d8049c41fbe197", + Redis: true, + } +) + +func TestCheck(t *testing.T) { + client, cleanup := setup(t) + defer cleanup() + + result, err := client.Check() + require.NoError(t, err) + require.Equal(t, testResponse, result) +} + +func setup(t *testing.T) (*Client, func()) { + url, cleanup := testserver.StartSocketHttpServer(t, requests) + + client, err := NewClient(&config.Config{GitlabUrl: url}) + require.NoError(t, err) + + return client, cleanup +} diff --git a/internal/gitlabnet/httpclient_test.go b/internal/gitlabnet/httpclient_test.go new file mode 100644 index 0000000..9b635bd --- /dev/null +++ b/internal/gitlabnet/httpclient_test.go @@ -0,0 +1,96 @@ +package gitlabnet + +import ( + "encoding/base64" + "fmt" + "io/ioutil" + "net/http" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" +) + +const ( + username = "basic_auth_user" + password = "basic_auth_password" +) + +func TestBasicAuthSettings(t *testing.T) { + requests := []testserver.TestRequestHandler{ + { + Path: "/api/v4/internal/get_endpoint", + Handler: func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + + fmt.Fprint(w, r.Header.Get("Authorization")) + }, + }, + { + Path: "/api/v4/internal/post_endpoint", + Handler: func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodPost, r.Method) + + fmt.Fprint(w, r.Header.Get("Authorization")) + }, + }, + } + config := &config.Config{HttpSettings: config.HttpSettingsConfig{User: username, Password: password}} + + client, cleanup := setup(t, config, requests) + defer cleanup() + + response, err := client.Get("/get_endpoint") + require.NoError(t, err) + testBasicAuthHeaders(t, response) + + response, err = client.Post("/post_endpoint", nil) + require.NoError(t, err) + testBasicAuthHeaders(t, response) +} + +func testBasicAuthHeaders(t *testing.T, response *http.Response) { + defer response.Body.Close() + + require.NotNil(t, response) + responseBody, err := ioutil.ReadAll(response.Body) + assert.NoError(t, err) + + headerParts := strings.Split(string(responseBody), " ") + assert.Equal(t, "Basic", headerParts[0]) + + credentials, err := base64.StdEncoding.DecodeString(headerParts[1]) + require.NoError(t, err) + + assert.Equal(t, username+":"+password, string(credentials)) +} + +func TestEmptyBasicAuthSettings(t *testing.T) { + requests := []testserver.TestRequestHandler{ + { + Path: "/api/v4/internal/empty_basic_auth", + Handler: func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "", r.Header.Get("Authorization")) + }, + }, + } + + client, cleanup := setup(t, &config.Config{}, requests) + defer cleanup() + + _, err := client.Get("/empty_basic_auth") + require.NoError(t, err) +} + +func setup(t *testing.T, config *config.Config, requests []testserver.TestRequestHandler) (*GitlabClient, func()) { + url, cleanup := testserver.StartHttpServer(t, requests) + + config.GitlabUrl = url + client, err := GetClient(config) + require.NoError(t, err) + + return client, cleanup +} diff --git a/internal/gitlabnet/httpsclient_test.go b/internal/gitlabnet/httpsclient_test.go new file mode 100644 index 0000000..04901df --- /dev/null +++ b/internal/gitlabnet/httpsclient_test.go @@ -0,0 +1,125 @@ +package gitlabnet + +import ( + "fmt" + "io/ioutil" + "net/http" + "path" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" +) + +func TestSuccessfulRequests(t *testing.T) { + testCases := []struct { + desc string + config *config.Config + }{ + { + desc: "Valid CaFile", + config: &config.Config{ + HttpSettings: config.HttpSettingsConfig{CaFile: path.Join(testhelper.TestRoot, "certs/valid/server.crt")}, + }, + }, + { + desc: "Valid CaPath", + config: &config.Config{ + HttpSettings: config.HttpSettingsConfig{CaPath: path.Join(testhelper.TestRoot, "certs/valid")}, + }, + }, + { + desc: "Self signed cert option enabled", + config: &config.Config{ + HttpSettings: config.HttpSettingsConfig{SelfSignedCert: true}, + }, + }, + { + desc: "Invalid cert with self signed cert option enabled", + config: &config.Config{ + HttpSettings: config.HttpSettingsConfig{SelfSignedCert: true, CaFile: path.Join(testhelper.TestRoot, "certs/valid/server.crt")}, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + client, cleanup := setupWithRequests(t, tc.config) + defer cleanup() + + response, err := client.Get("/hello") + require.NoError(t, err) + require.NotNil(t, response) + + defer response.Body.Close() + + responseBody, err := ioutil.ReadAll(response.Body) + assert.NoError(t, err) + assert.Equal(t, string(responseBody), "Hello") + }) + } +} + +func TestFailedRequests(t *testing.T) { + testCases := []struct { + desc string + config *config.Config + }{ + { + desc: "Invalid CaFile", + config: &config.Config{ + HttpSettings: config.HttpSettingsConfig{CaFile: path.Join(testhelper.TestRoot, "certs/invalid/server.crt")}, + }, + }, + { + desc: "Invalid CaPath", + config: &config.Config{ + HttpSettings: config.HttpSettingsConfig{CaPath: path.Join(testhelper.TestRoot, "certs/invalid")}, + }, + }, + { + desc: "Empty config", + config: &config.Config{}, + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + client, cleanup := setupWithRequests(t, tc.config) + defer cleanup() + + _, err := client.Get("/hello") + require.Error(t, err) + + assert.Equal(t, err.Error(), "Internal API unreachable") + }) + } +} + +func setupWithRequests(t *testing.T, config *config.Config) (*GitlabClient, func()) { + testDirCleanup, err := testhelper.PrepareTestRootDir() + require.NoError(t, err) + defer testDirCleanup() + + requests := []testserver.TestRequestHandler{ + { + Path: "/api/v4/internal/hello", + Handler: func(w http.ResponseWriter, r *http.Request) { + require.Equal(t, http.MethodGet, r.Method) + + fmt.Fprint(w, "Hello") + }, + }, + } + + url, cleanup := testserver.StartHttpsServer(t, requests) + + config.GitlabUrl = url + client, err := GetClient(config) + require.NoError(t, err) + + return client, cleanup +} diff --git a/internal/gitlabnet/lfsauthenticate/client.go b/internal/gitlabnet/lfsauthenticate/client.go new file mode 100644 index 0000000..51cb7a4 --- /dev/null +++ b/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.Shell +} + +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.Shell) (*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/internal/gitlabnet/lfsauthenticate/client_test.go b/internal/gitlabnet/lfsauthenticate/client_test.go new file mode 100644 index 0000000..07484a7 --- /dev/null +++ b/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.Shell + expectedOutput string + }{ + { + desc: "With bad response", + args: &commandargs.Shell{GitlabKeyId: "-1", CommandType: commandargs.UploadPack}, + expectedOutput: "Parsing failed", + }, + { + desc: "With API returns an error", + args: &commandargs.Shell{GitlabKeyId: "forbidden", CommandType: commandargs.UploadPack}, + expectedOutput: "Internal API error (403)", + }, + { + desc: "With API fails", + args: &commandargs.Shell{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.Shell{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) +} diff --git a/internal/gitlabnet/testserver/gitalyserver.go b/internal/gitlabnet/testserver/gitalyserver.go new file mode 100644 index 0000000..694fd41 --- /dev/null +++ b/internal/gitlabnet/testserver/gitalyserver.go @@ -0,0 +1,78 @@ +package testserver + +import ( + "io/ioutil" + "net" + "os" + "path" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + + pb "gitlab.com/gitlab-org/gitaly/proto/go/gitalypb" +) + +type testGitalyServer struct{} + +func (s *testGitalyServer) SSHReceivePack(stream pb.SSHService_SSHReceivePackServer) error { + req, err := stream.Recv() + if err != nil { + return err + } + + response := []byte("ReceivePack: " + req.GlId + " " + req.Repository.GlRepository) + stream.Send(&pb.SSHReceivePackResponse{Stdout: response}) + + return nil +} + +func (s *testGitalyServer) SSHUploadPack(stream pb.SSHService_SSHUploadPackServer) error { + req, err := stream.Recv() + if err != nil { + return err + } + + response := []byte("UploadPack: " + req.Repository.GlRepository) + stream.Send(&pb.SSHUploadPackResponse{Stdout: response}) + + return nil +} + +func (s *testGitalyServer) SSHUploadArchive(stream pb.SSHService_SSHUploadArchiveServer) error { + req, err := stream.Recv() + if err != nil { + return err + } + + response := []byte("UploadArchive: " + req.Repository.GlRepository) + stream.Send(&pb.SSHUploadArchiveResponse{Stdout: response}) + + return nil +} + +func StartGitalyServer(t *testing.T) (string, func()) { + tempDir, _ := ioutil.TempDir("", "gitlab-shell-test-api") + gitalySocketPath := path.Join(tempDir, "gitaly.sock") + + err := os.MkdirAll(filepath.Dir(gitalySocketPath), 0700) + require.NoError(t, err) + + server := grpc.NewServer() + + listener, err := net.Listen("unix", gitalySocketPath) + require.NoError(t, err) + + pb.RegisterSSHServiceServer(server, &testGitalyServer{}) + + go server.Serve(listener) + + gitalySocketUrl := "unix:" + gitalySocketPath + cleanup := func() { + server.Stop() + os.RemoveAll(tempDir) + } + + return gitalySocketUrl, cleanup +} diff --git a/internal/gitlabnet/testserver/testserver.go b/internal/gitlabnet/testserver/testserver.go new file mode 100644 index 0000000..bf59ce4 --- /dev/null +++ b/internal/gitlabnet/testserver/testserver.go @@ -0,0 +1,82 @@ +package testserver + +import ( + "crypto/tls" + "io/ioutil" + "log" + "net" + "net/http" + "net/http/httptest" + "os" + "path" + "path/filepath" + "testing" + + "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" +) + +var ( + tempDir, _ = ioutil.TempDir("", "gitlab-shell-test-api") + testSocket = path.Join(tempDir, "internal.sock") +) + +type TestRequestHandler struct { + Path string + Handler func(w http.ResponseWriter, r *http.Request) +} + +func StartSocketHttpServer(t *testing.T, handlers []TestRequestHandler) (string, func()) { + err := os.MkdirAll(filepath.Dir(testSocket), 0700) + require.NoError(t, err) + + socketListener, err := net.Listen("unix", testSocket) + require.NoError(t, err) + + server := http.Server{ + Handler: buildHandler(handlers), + // We'll put this server through some nasty stuff we don't want + // in our test output + ErrorLog: log.New(ioutil.Discard, "", 0), + } + go server.Serve(socketListener) + + url := "http+unix://" + testSocket + + return url, cleanupSocket +} + +func StartHttpServer(t *testing.T, handlers []TestRequestHandler) (string, func()) { + server := httptest.NewServer(buildHandler(handlers)) + + return server.URL, server.Close +} + +func StartHttpsServer(t *testing.T, handlers []TestRequestHandler) (string, func()) { + crt := path.Join(testhelper.TestRoot, "certs/valid/server.crt") + key := path.Join(testhelper.TestRoot, "certs/valid/server.key") + + server := httptest.NewUnstartedServer(buildHandler(handlers)) + cer, err := tls.LoadX509KeyPair(crt, key) + require.NoError(t, err) + + server.TLS = &tls.Config{Certificates: []tls.Certificate{cer}} + server.StartTLS() + + return server.URL, server.Close +} + +func cleanupSocket() { + os.RemoveAll(tempDir) +} + +func buildHandler(handlers []TestRequestHandler) http.Handler { + h := http.NewServeMux() + + for _, handler := range handlers { + h.HandleFunc(handler.Path, handler.Handler) + } + + return h +} diff --git a/internal/gitlabnet/twofactorrecover/client.go b/internal/gitlabnet/twofactorrecover/client.go new file mode 100644 index 0000000..37067db --- /dev/null +++ b/internal/gitlabnet/twofactorrecover/client.go @@ -0,0 +1,89 @@ +package twofactorrecover + +import ( + "errors" + "fmt" + "net/http" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/command/commandargs" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/discover" +) + +type Client struct { + config *config.Config + client *gitlabnet.GitlabClient +} + +type Response struct { + Success bool `json:"success"` + RecoveryCodes []string `json:"recovery_codes"` + Message string `json:"message"` +} + +type RequestBody struct { + KeyId string `json:"key_id,omitempty"` + UserId int64 `json:"user_id,omitempty"` +} + +func NewClient(config *config.Config) (*Client, error) { + client, err := gitlabnet.GetClient(config) + if err != nil { + return nil, fmt.Errorf("Error creating http client: %v", err) + } + + return &Client{config: config, client: client}, nil +} + +func (c *Client) GetRecoveryCodes(args *commandargs.Shell) ([]string, error) { + requestBody, err := c.getRequestBody(args) + + if err != nil { + return nil, err + } + + response, err := c.client.Post("/two_factor_recovery_codes", requestBody) + if err != nil { + return nil, err + } + defer response.Body.Close() + + return parse(response) +} + +func parse(hr *http.Response) ([]string, error) { + response := &Response{} + if err := gitlabnet.ParseJSON(hr, response); err != nil { + return nil, err + } + + if !response.Success { + return nil, errors.New(response.Message) + } + + return response.RecoveryCodes, nil +} + +func (c *Client) getRequestBody(args *commandargs.Shell) (*RequestBody, error) { + client, err := discover.NewClient(c.config) + + if err != nil { + return nil, err + } + + var requestBody *RequestBody + if args.GitlabKeyId != "" { + requestBody = &RequestBody{KeyId: args.GitlabKeyId} + } else { + userInfo, err := client.GetByCommandArgs(args) + + if err != nil { + return nil, err + } + + requestBody = &RequestBody{UserId: userInfo.UserId} + } + + return requestBody, nil +} diff --git a/internal/gitlabnet/twofactorrecover/client_test.go b/internal/gitlabnet/twofactorrecover/client_test.go new file mode 100644 index 0000000..a560fb1 --- /dev/null +++ b/internal/gitlabnet/twofactorrecover/client_test.go @@ -0,0 +1,158 @@ +package twofactorrecover + +import ( + "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/config" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/discover" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" +) + +var ( + requests []testserver.TestRequestHandler +) + +func initialize(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 *RequestBody + json.Unmarshal(b, &requestBody) + + switch requestBody.KeyId { + case "0": + body := map[string]interface{}{ + "success": true, + "recovery_codes": [2]string{"recovery 1", "codes 1"}, + } + json.NewEncoder(w).Encode(body) + case "1": + body := map[string]interface{}{ + "success": false, + "message": "missing user", + } + json.NewEncoder(w).Encode(body) + case "2": + w.WriteHeader(http.StatusForbidden) + body := &gitlabnet.ErrorResponse{ + Message: "Not allowed!", + } + json.NewEncoder(w).Encode(body) + case "3": + w.Write([]byte("{ \"message\": \"broken json!\"")) + case "4": + w.WriteHeader(http.StatusForbidden) + } + + if requestBody.UserId == 1 { + body := map[string]interface{}{ + "success": true, + "recovery_codes": [2]string{"recovery 2", "codes 2"}, + } + json.NewEncoder(w).Encode(body) + } + }, + }, + { + Path: "/api/v4/internal/discover", + Handler: func(w http.ResponseWriter, r *http.Request) { + body := &discover.Response{ + UserId: 1, + Username: "jane-doe", + Name: "Jane Doe", + } + json.NewEncoder(w).Encode(body) + }, + }, + } +} + +func TestGetRecoveryCodesByKeyId(t *testing.T) { + client, cleanup := setup(t) + defer cleanup() + + args := &commandargs.Shell{GitlabKeyId: "0"} + result, err := client.GetRecoveryCodes(args) + assert.NoError(t, err) + assert.Equal(t, []string{"recovery 1", "codes 1"}, result) +} + +func TestGetRecoveryCodesByUsername(t *testing.T) { + client, cleanup := setup(t) + defer cleanup() + + args := &commandargs.Shell{GitlabUsername: "jane-doe"} + result, err := client.GetRecoveryCodes(args) + assert.NoError(t, err) + assert.Equal(t, []string{"recovery 2", "codes 2"}, result) +} + +func TestMissingUser(t *testing.T) { + client, cleanup := setup(t) + defer cleanup() + + args := &commandargs.Shell{GitlabKeyId: "1"} + _, err := client.GetRecoveryCodes(args) + assert.Equal(t, "missing user", err.Error()) +} + +func TestErrorResponses(t *testing.T) { + client, cleanup := setup(t) + defer cleanup() + + testCases := []struct { + desc string + fakeId string + expectedError string + }{ + { + desc: "A response with an error message", + fakeId: "2", + expectedError: "Not allowed!", + }, + { + desc: "A response with bad JSON", + fakeId: "3", + expectedError: "Parsing failed", + }, + { + desc: "An error response without message", + fakeId: "4", + expectedError: "Internal API error (403)", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + args := &commandargs.Shell{GitlabKeyId: tc.fakeId} + resp, err := client.GetRecoveryCodes(args) + + assert.EqualError(t, err, tc.expectedError) + assert.Nil(t, resp) + }) + } +} + +func setup(t *testing.T) (*Client, func()) { + initialize(t) + url, cleanup := testserver.StartSocketHttpServer(t, requests) + + client, err := NewClient(&config.Config{GitlabUrl: url}) + require.NoError(t, err) + + return client, cleanup +} diff --git a/internal/handler/exec.go b/internal/handler/exec.go new file mode 100644 index 0000000..1f3177d --- /dev/null +++ b/internal/handler/exec.go @@ -0,0 +1,96 @@ +package handler + +import ( + "context" + "fmt" + "os" + + "gitlab.com/gitlab-org/gitaly/auth" + "gitlab.com/gitlab-org/gitaly/client" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" + "gitlab.com/gitlab-org/labkit/tracing" + "google.golang.org/grpc" +) + +// GitalyHandlerFunc implementations are responsible for making +// an appropriate Gitaly call using the provided client and context +// and returning an error from the Gitaly call. +type GitalyHandlerFunc func(ctx context.Context, client *grpc.ClientConn) (int32, error) + +type GitalyConn struct { + ctx context.Context + conn *grpc.ClientConn + close func() +} + +type GitalyCommand struct { + Config *config.Config + ServiceName string + Address string + Token string +} + +// RunGitalyCommand provides a bootstrap for Gitaly commands executed +// through GitLab-Shell. It ensures that logging, tracing and other +// common concerns are configured before executing the `handler`. +func (gc *GitalyCommand) RunGitalyCommand(handler GitalyHandlerFunc) error { + gitalyConn, err := getConn(gc) + + if err != nil { + return err + } + + _, err = handler(gitalyConn.ctx, gitalyConn.conn) + + gitalyConn.close() + + return err +} + +func getConn(gc *GitalyCommand) (*GitalyConn, error) { + if gc.Address == "" { + return nil, fmt.Errorf("no gitaly_address given") + } + + connOpts := client.DefaultDialOpts + if gc.Token != "" { + connOpts = append(client.DefaultDialOpts, grpc.WithPerRPCCredentials(gitalyauth.RPCCredentialsV2(gc.Token))) + } + + // Use a working directory that won't get removed or unmounted. + if err := os.Chdir("/"); err != nil { + return nil, err + } + + // Configure distributed tracing + serviceName := fmt.Sprintf("gitlab-shell-%v", gc.ServiceName) + closer := tracing.Initialize( + tracing.WithServiceName(serviceName), + + // For GitLab-Shell, we explicitly initialize tracing from a config file + // instead of the default environment variable (using GITLAB_TRACING) + // This decision was made owing to the difficulty in passing environment + // variables into GitLab-Shell processes. + // + // Processes are spawned as children of the SSH daemon, which tightly + // controls environment variables; doing this means we don't have to + // enable PermitUserEnvironment + tracing.WithConnectionString(gc.Config.GitlabTracing), + ) + + ctx, finished := tracing.ExtractFromEnv(context.Background()) + + conn, err := client.Dial(gc.Address, connOpts) + if err != nil { + return nil, err + } + + finish := func() { + finished() + closer.Close() + conn.Close() + } + + return &GitalyConn{ctx: ctx, conn: conn, close: finish}, nil +} diff --git a/internal/handler/exec_test.go b/internal/handler/exec_test.go new file mode 100644 index 0000000..6c7d3f5 --- /dev/null +++ b/internal/handler/exec_test.go @@ -0,0 +1,42 @@ +package handler + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/require" + "google.golang.org/grpc" + + "gitlab.com/gitlab-org/gitlab-shell/internal/config" +) + +func makeHandler(t *testing.T, err error) func(context.Context, *grpc.ClientConn) (int32, error) { + return func(ctx context.Context, client *grpc.ClientConn) (int32, error) { + require.NotNil(t, ctx) + require.NotNil(t, client) + + return 0, err + } +} + +func TestRunGitalyCommand(t *testing.T) { + cmd := GitalyCommand{ + Config: &config.Config{}, + Address: "tcp://localhost:9999", + } + + err := cmd.RunGitalyCommand(makeHandler(t, nil)) + require.NoError(t, err) + + expectedErr := errors.New("error") + err = cmd.RunGitalyCommand(makeHandler(t, expectedErr)) + require.Equal(t, err, expectedErr) +} + +func TestMissingGitalyAddress(t *testing.T) { + cmd := GitalyCommand{Config: &config.Config{}} + + err := cmd.RunGitalyCommand(makeHandler(t, nil)) + require.EqualError(t, err, "no gitaly_address given") +} diff --git a/internal/keyline/key_line.go b/internal/keyline/key_line.go new file mode 100644 index 0000000..f92f50b --- /dev/null +++ b/internal/keyline/key_line.go @@ -0,0 +1,62 @@ +package keyline + +import ( + "errors" + "fmt" + "path" + "regexp" + "strings" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/executable" +) + +var ( + keyRegex = regexp.MustCompile(`\A[a-z0-9-]+\z`) +) + +const ( + PublicKeyPrefix = "key" + PrincipalPrefix = "username" + SshOptions = "no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty" +) + +type KeyLine struct { + Id string // This can be either an ID of a Key or username + Value string // This can be either a public key or a principal name + Prefix string + RootDir string +} + +func NewPublicKeyLine(id string, publicKey string, rootDir string) (*KeyLine, error) { + return newKeyLine(id, publicKey, PublicKeyPrefix, rootDir) +} + +func NewPrincipalKeyLine(keyId string, principal string, rootDir string) (*KeyLine, error) { + return newKeyLine(keyId, principal, PrincipalPrefix, rootDir) +} + +func (k *KeyLine) ToString() string { + command := fmt.Sprintf("%s %s-%s", path.Join(k.RootDir, executable.BinDir, executable.GitlabShell), k.Prefix, k.Id) + + return fmt.Sprintf(`command="%s",%s %s`, command, SshOptions, k.Value) +} + +func newKeyLine(id string, value string, prefix string, rootDir string) (*KeyLine, error) { + if err := validate(id, value); err != nil { + return nil, err + } + + return &KeyLine{Id: id, Value: value, Prefix: prefix, RootDir: rootDir}, nil +} + +func validate(id string, value string) error { + if !keyRegex.MatchString(id) { + return errors.New(fmt.Sprintf("Invalid key_id: %s", id)) + } + + if strings.Contains(value, "\n") { + return errors.New(fmt.Sprintf("Invalid value: %s", value)) + } + + return nil +} diff --git a/internal/keyline/key_line_test.go b/internal/keyline/key_line_test.go new file mode 100644 index 0000000..c6883c0 --- /dev/null +++ b/internal/keyline/key_line_test.go @@ -0,0 +1,82 @@ +package keyline + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestFailingNewPublicKeyLine(t *testing.T) { + testCases := []struct { + desc string + id string + publicKey string + expectedError string + }{ + { + desc: "When Id has non-alphanumeric and non-dash characters in it", + id: "key\n1", + publicKey: "public-key", + expectedError: "Invalid key_id: key\n1", + }, + { + desc: "When public key has newline in it", + id: "key", + publicKey: "public\nkey", + expectedError: "Invalid value: public\nkey", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + result, err := NewPublicKeyLine(tc.id, tc.publicKey, "root-dir") + + require.Empty(t, result) + require.EqualError(t, err, tc.expectedError) + }) + } +} + +func TestFailingNewPrincipalKeyLine(t *testing.T) { + testCases := []struct { + desc string + keyId string + principal string + expectedError string + }{ + { + desc: "When username has non-alphanumeric and non-dash characters in it", + keyId: "username\n1", + principal: "principal", + expectedError: "Invalid key_id: username\n1", + }, + { + desc: "When principal has newline in it", + keyId: "username", + principal: "principal\n1", + expectedError: "Invalid value: principal\n1", + }, + } + + for _, tc := range testCases { + t.Run(tc.desc, func(t *testing.T) { + result, err := NewPrincipalKeyLine(tc.keyId, tc.principal, "root-dir") + + require.Empty(t, result) + require.EqualError(t, err, tc.expectedError) + }) + } +} + +func TestToString(t *testing.T) { + keyLine := &KeyLine{ + Id: "1", + Value: "public-key", + Prefix: "key", + RootDir: "/tmp", + } + + result := keyLine.ToString() + + require.Equal(t, `command="/tmp/bin/gitlab-shell key-1",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty public-key`, result) +} diff --git a/internal/logger/logger.go b/internal/logger/logger.go new file mode 100644 index 0000000..c356d43 --- /dev/null +++ b/internal/logger/logger.go @@ -0,0 +1,82 @@ +package logger + +import ( + "fmt" + "io" + golog "log" + "log/syslog" + "os" + "sync" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/config" + + log "github.com/sirupsen/logrus" +) + +var ( + logWriter io.Writer + bootstrapLogger *golog.Logger + pid int + mutex sync.Mutex + ProgName string +) + +func Configure(cfg *config.Config) error { + mutex.Lock() + defer mutex.Unlock() + + pid = os.Getpid() + + var err error + logWriter, err = os.OpenFile(cfg.LogFile, os.O_WRONLY|os.O_APPEND, 0) + if err != nil { + return err + } + + log.SetOutput(logWriter) + if cfg.LogFormat == "json" { + log.SetFormatter(&log.JSONFormatter{}) + } + + return nil +} + +func logPrint(msg string, err error) { + mutex.Lock() + defer mutex.Unlock() + + if logWriter == nil { + bootstrapLogPrint(msg, err) + return + } + + log.WithError(err).WithFields(log.Fields{ + "pid": pid, + }).Error(msg) +} + +func Fatal(msg string, err error) { + logPrint(msg, err) + // We don't show the error to the end user because it can leak + // information that is private to the GitLab server. + fmt.Fprintf(os.Stderr, "%s: fatal: %s\n", ProgName, msg) + os.Exit(1) +} + +// If our log file is not available we want to log somewhere else, but +// not to standard error because that leaks information to the user. This +// function attemps to log to syslog. +// +// We assume the logging mutex is already locked. +func bootstrapLogPrint(msg string, err error) { + if bootstrapLogger == nil { + var err error + bootstrapLogger, err = syslog.NewLogger(syslog.LOG_ERR|syslog.LOG_USER, 0) + if err != nil { + // The message will not be logged. + return + } + } + + bootstrapLogger.Print(ProgName+":", msg+":", err) +} diff --git a/internal/sshenv/sshenv.go b/internal/sshenv/sshenv.go new file mode 100644 index 0000000..387feb2 --- /dev/null +++ b/internal/sshenv/sshenv.go @@ -0,0 +1,15 @@ +package sshenv + +import ( + "os" + "strings" +) + +func LocalAddr() string { + address := os.Getenv("SSH_CONNECTION") + + if address != "" { + return strings.Fields(address)[0] + } + return "" +} diff --git a/internal/sshenv/sshenv_test.go b/internal/sshenv/sshenv_test.go new file mode 100644 index 0000000..d2207f5 --- /dev/null +++ b/internal/sshenv/sshenv_test.go @@ -0,0 +1,20 @@ +package sshenv + +import ( + "testing" + + "github.com/stretchr/testify/require" + "gitlab.com/gitlab-org/gitlab-shell/go/internal/testhelper" +) + +func TestLocalAddr(t *testing.T) { + cleanup, err := testhelper.Setenv("SSH_CONNECTION", "127.0.0.1 0") + require.NoError(t, err) + defer cleanup() + + require.Equal(t, LocalAddr(), "127.0.0.1") +} + +func TestEmptyLocalAddr(t *testing.T) { + require.Equal(t, LocalAddr(), "") +} diff --git a/internal/testhelper/requesthandlers/requesthandlers.go b/internal/testhelper/requesthandlers/requesthandlers.go new file mode 100644 index 0000000..a7bc427 --- /dev/null +++ b/internal/testhelper/requesthandlers/requesthandlers.go @@ -0,0 +1,58 @@ +package requesthandlers + +import ( + "encoding/json" + "net/http" + "testing" + + "github.com/stretchr/testify/require" + + "gitlab.com/gitlab-org/gitlab-shell/go/internal/gitlabnet/testserver" +) + +func BuildDisallowedByApiHandlers(t *testing.T) []testserver.TestRequestHandler { + requests := []testserver.TestRequestHandler{ + { + Path: "/api/v4/internal/allowed", + Handler: func(w http.ResponseWriter, r *http.Request) { + body := map[string]interface{}{ + "status": false, + "message": "Disallowed by API call", + } + w.WriteHeader(http.StatusForbidden) + require.NoError(t, json.NewEncoder(w).Encode(body)) + }, + }, + } + + return requests +} + +func BuildAllowedWithGitalyHandlers(t *testing.T, gitalyAddress string) []testserver.TestRequestHandler { + requests := []testserver.TestRequestHandler{ + { + Path: "/api/v4/internal/allowed", + Handler: func(w http.ResponseWriter, r *http.Request) { + body := map[string]interface{}{ + "status": true, + "gl_id": "1", + "gitaly": map[string]interface{}{ + "repository": map[string]interface{}{ + "storage_name": "storage_name", + "relative_path": "relative_path", + "git_object_directory": "path/to/git_object_directory", + "git_alternate_object_directories": []string{"path/to/git_alternate_object_directory"}, + "gl_repository": "group/repo", + "gl_project_path": "group/project-path", + }, + "address": gitalyAddress, + "token": "token", + }, + } + require.NoError(t, json.NewEncoder(w).Encode(body)) + }, + }, + } + + return requests +} diff --git a/internal/testhelper/testdata/testroot/.gitlab_shell_secret b/internal/testhelper/testdata/testroot/.gitlab_shell_secret new file mode 100644 index 0000000..9bd459d --- /dev/null +++ b/internal/testhelper/testdata/testroot/.gitlab_shell_secret @@ -0,0 +1 @@ +default-secret-content \ No newline at end of file diff --git a/internal/testhelper/testdata/testroot/certs/invalid/server.crt b/internal/testhelper/testdata/testroot/certs/invalid/server.crt new file mode 100644 index 0000000..f8a42c1 --- /dev/null +++ b/internal/testhelper/testdata/testroot/certs/invalid/server.crt @@ -0,0 +1,10 @@ +-----BEGIN CERTIFICATE----- +MinvalidcertAOvHjs6cs1R9MAoGCCqGSM49BAMCMBQxEjAQBgNVBAMMCWxvY2Fs +ainvalidcertOTA0MjQxNjM4NTBaFw0yOTA0MjExNjM4NTBaMBQxEjAQBgNVBAMM +CinvalidcertdDB2MBAGByqGSM49AgEGBSuBBAAiA2IABJ5m7oW9OuL7aTAC04sL +3invalidcertdB2L0GsVCImav4PEpx6UAjkoiNGW9j0zPdNgxTYDjiCaGmr1aY2X +kinvalidcert7MNq7H8v7Ce/vrKkcDMOX8Gd/ddT3dEVqzAKBggqhkjOPQQDAgNp +AinvalidcertswcyjiB+A+ZjMSfaOsA2hAP0I3fkTcry386DePViMfnaIjm7rcuu +Jinvalidcert5V5CHypOxio1tOtGjaDkSH2FCdoatMyIe02+F6TIo44i4J/zjN52 +Jinvalidcert +-----END CERTIFICATE----- diff --git a/internal/testhelper/testdata/testroot/certs/valid/dir/.gitkeep b/internal/testhelper/testdata/testroot/certs/valid/dir/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/internal/testhelper/testdata/testroot/certs/valid/server.crt b/internal/testhelper/testdata/testroot/certs/valid/server.crt new file mode 100644 index 0000000..11f1da7 --- /dev/null +++ b/internal/testhelper/testdata/testroot/certs/valid/server.crt @@ -0,0 +1,22 @@ +-----BEGIN CERTIFICATE----- +MIIDrjCCApagAwIBAgIUHVNTmyz3p+7xSEMkSfhPz4BZfqwwDQYJKoZIhvcNAQEL +BQAwTjELMAkGA1UEBhMCVVMxEzARBgNVBAgMCkNhbGlmb3JuaWExEjAQBgNVBAcM +CVRoZSBDbG91ZDEWMBQGA1UECgwNTXkgQ29tcGFueSBDQTAeFw0xOTA5MjAxMDQ3 +NTlaFw0yOTA5MTcxMDQ3NTlaMF4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIDApDYWxp +Zm9ybmlhMRIwEAYDVQQHDAlUaGUgQ2xvdWQxDTALBgNVBAoMBERlbW8xFzAVBgNV +BAMMDk15IENlcnRpZmljYXRlMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC +AQEAmte3G/eD+quamwyFl+2jEo8ngSAT0FWeY5ZAwRvdF4FgtTBLvbAdTnyi7pHM +esCSUkyxXHHPazM4SDV6uiu5LNKF0iz/NY76rLtFoqSGUgygTZHVbZ6NRXCNUZ0P +slD95wOCWvS9t9xgNXry66k8+mfZNhE+cFQfrO/pN5WpNuGyWTfKlUQw5NVL3mob +j3tSjI+wzSpbPMvbTQoBiZ/VHkyyc15YdrbePwFB2dJbxE/Xgsyk/TwWSUFnAs6i +1x2t+423NIm9rIDTdW2YYJJXv3MUcdDIxJnY0beGePMIymn9ZIRUJtK/ZXmwMb52 +v70+YTcsG67uSm31CR8jNt8qpQIDAQABo3QwcjAJBgNVHRMEAjAAMB0GA1UdDgQW +BBTxZ9SORmIwDs90TW8UXIVhDst4kjALBgNVHQ8EBAMCBaAwHQYDVR0lBBYwFAYI +KwYBBQUHAwIGCCsGAQUFBwMBMBoGA1UdEQQTMBGHBH8AAAGCCWxvY2FsaG9zdDAN +BgkqhkiG9w0BAQsFAAOCAQEAf4Iq94Su9TlkReMS4x2N5xZru9YoKQtrrxqWSRbp +oh5Lwtk9rJPy6q4IEPXzDsRI1YWCZe1Fw7zdiNfmoFRxjs59MBJ9YVrcFeyeAILg +LiAiRcGth2THpikCnLxmniGHUUX1WfjmcDEYMIs6BZ98N64VWwtuZqcJnJPmQs64 +lDrgW9oz6/8hPMeW58ok8PjkiG+E+srBaURoKwNe7vfPRVyq45N67/juH+4o6QBd +WP6ACjDM3RnxyWyW0S+sl3i3EAGgtwM6RIDhOG238HOIiA/I/+CCmITsvujz6jMN +bLdoPfnatZ7f5m9DuoOsGlYAZbLfOl2NywgO0jAlnHJGEQ== +-----END CERTIFICATE----- diff --git a/internal/testhelper/testdata/testroot/certs/valid/server.key b/internal/testhelper/testdata/testroot/certs/valid/server.key new file mode 100644 index 0000000..acec0fb --- /dev/null +++ b/internal/testhelper/testdata/testroot/certs/valid/server.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEogIBAAKCAQEAmte3G/eD+quamwyFl+2jEo8ngSAT0FWeY5ZAwRvdF4FgtTBL +vbAdTnyi7pHMesCSUkyxXHHPazM4SDV6uiu5LNKF0iz/NY76rLtFoqSGUgygTZHV +bZ6NRXCNUZ0PslD95wOCWvS9t9xgNXry66k8+mfZNhE+cFQfrO/pN5WpNuGyWTfK +lUQw5NVL3mobj3tSjI+wzSpbPMvbTQoBiZ/VHkyyc15YdrbePwFB2dJbxE/Xgsyk +/TwWSUFnAs6i1x2t+423NIm9rIDTdW2YYJJXv3MUcdDIxJnY0beGePMIymn9ZIRU +JtK/ZXmwMb52v70+YTcsG67uSm31CR8jNt8qpQIDAQABAoIBAEJQyNdtdlTRUfG9 +tymOWR0FuoGO322GfcNhAnKyIEqE2oo/GPEwkByhPJa4Ur7v4rrkpcFV7OOYmC40 +2U8KktAjibSuGM8zYSDBQ92YYP6a8bzHDIVaNl7bCWs+vQ49qcBavGWAFBC+jWXa +Nle/r6H/AAQr9nXdUYObbGKl8kbSUBNAqQHILsNyxQsAo12oqRnUWhIbfzUFBr1m +us93OsvpOYWgkbaBWk0brjp2X0eNGHctTboFxRknJcU6MQVL5degbgXhnCm4ir4O +E2KMubEwxePr5fPotWNQXCVin85OQv1eb70anfwoA2b5/ykb57jo5EDoiUoFsjLz +KLAaRQECgYEAzZNP/CpwCh5s31SDr7ajYfNIu8ie370g2Qbf4jrqVrOJ8Sj1LRYB +lS5+QbSRu4W6Ani3AQwZA09lS608G8w5rD7YGRVDCFuwJt+Yz5GcsSkso9B8DR4h +vCe2WuDutz7M5ikP1DAc/9x5HIzjQijxM1JJCNU2nR6QoFvV6wpVcpECgYEAwNK9 +oTqyb7UjNinAo9PFrFpnbX+DoGokGPsRyUwi9UkyRR0Uf7Kxjoq2C8zsCvnGdrE7 +kwUiWjyfAgMDF8+iWHYO1vD7m6NL31h/AAmo0NEQIBs0LFj0lF0xORzvXdTjhvuG +LxXhm927z4WBOCLTn8FAsBUjVBpmB6ffyZCVWNUCgYA3P4j2fz0/KvAdkSwW9CGy +uFxqwz8XaE/Eo9lVhnnmNTg0TMqfhFOGkUkzRWEJIaZc9a5RJLwwLI1Pqk4GNnul +c/pFu3YZb/LGb780wbB32FX77JL6P4fXdmDGyb6+Fq2giZaMcyXICauu5ZpJ9JDm +Nw4TxqF31ngN8MBr+4n9UQKBgAkxAoEQ/zh79fW6/8fPbHjOxmdd0LRw2s+mCC8E +RhZTKuZIgJWluvkEe7EMT6QmS+OUhzZ25DBQ+3NpGVilOSPmXMa6LgQ5QIChA0zJ +KRbrIE2nflEu3FnGJ3aFfpOGdmIU00yjSmHXrAA0aPh4EIZo++Bo4Yo8x+hNhElj +bvsRAoGADYZTUchbiVndk5QtnbwlDjrF5PmgjoDboBfv9/6FU+DzQRyOpl3kr0hs +OcZGE6xPZJidv1Bcv60L1VzTMj7spvMRTeumn2zEQGjkl6i/fSZzawjmKaKXKNkC +YfoV0RepB4TlNYGICaTcV+aKRIXivcpBGfduZEb39iUKCjh9Afg= +-----END RSA PRIVATE KEY----- diff --git a/internal/testhelper/testdata/testroot/config.yml b/internal/testhelper/testdata/testroot/config.yml new file mode 100644 index 0000000..e69de29 diff --git a/internal/testhelper/testdata/testroot/custom/my-contents-is-secret b/internal/testhelper/testdata/testroot/custom/my-contents-is-secret new file mode 100644 index 0000000..645b575 --- /dev/null +++ b/internal/testhelper/testdata/testroot/custom/my-contents-is-secret @@ -0,0 +1 @@ +custom-secret-content \ No newline at end of file diff --git a/internal/testhelper/testdata/testroot/gitlab-shell.log b/internal/testhelper/testdata/testroot/gitlab-shell.log new file mode 100644 index 0000000..e69de29 diff --git a/internal/testhelper/testdata/testroot/responses/allowed.json b/internal/testhelper/testdata/testroot/responses/allowed.json new file mode 100644 index 0000000..d0403d9 --- /dev/null +++ b/internal/testhelper/testdata/testroot/responses/allowed.json @@ -0,0 +1,22 @@ +{ + "status": true, + "gl_repository": "project-26", + "gl_project_path": "group/private", + "gl_id": "user-1", + "gl_username": "root", + "git_config_options": ["option"], + "gitaly": { + "repository": { + "storage_name": "default", + "relative_path": "@hashed/5f/9c/5f9c4ab08cac7457e9111a30e4664920607ea2c115a1433d7be98e97e64244ca.git", + "git_object_directory": "path/to/git_object_directory", + "git_alternate_object_directories": ["path/to/git_alternate_object_directory"], + "gl_repository": "project-26", + "gl_project_path": "group/private" + }, + "address": "unix:gitaly.socket", + "token": "token" + }, + "git_protocol": "protocol", + "gl_console_messages": ["console", "message"] +} diff --git a/internal/testhelper/testdata/testroot/responses/allowed_with_payload.json b/internal/testhelper/testdata/testroot/responses/allowed_with_payload.json new file mode 100644 index 0000000..331c3a9 --- /dev/null +++ b/internal/testhelper/testdata/testroot/responses/allowed_with_payload.json @@ -0,0 +1,31 @@ +{ + "status": true, + "gl_repository": "project-26", + "gl_project_path": "group/private", + "gl_id": "user-1", + "gl_username": "root", + "git_config_options": ["option"], + "gitaly": { + "repository": { + "storage_name": "default", + "relative_path": "@hashed/5f/9c/5f9c4ab08cac7457e9111a30e4664920607ea2c115a1433d7be98e97e64244ca.git", + "git_object_directory": "path/to/git_object_directory", + "git_alternate_object_directories": ["path/to/git_alternate_object_directory"], + "gl_repository": "project-26", + "gl_project_path": "group/private" + }, + "address": "unix:gitaly.socket", + "token": "token" + }, + "payload" : { + "action": "geo_proxy_to_primary", + "data": { + "api_endpoints": ["geo/proxy_git_push_ssh/info_refs", "geo/proxy_git_push_ssh/push"], + "gl_username": "custom", + "primary_repo": "https://repo/path", + "info_message": "message" + } + }, + "git_protocol": "protocol", + "gl_console_messages": ["console", "message"] +} diff --git a/internal/testhelper/testhelper.go b/internal/testhelper/testhelper.go new file mode 100644 index 0000000..a925c79 --- /dev/null +++ b/internal/testhelper/testhelper.go @@ -0,0 +1,93 @@ +package testhelper + +import ( + "fmt" + "io/ioutil" + "os" + "path" + "runtime" + + "github.com/otiai10/copy" +) + +var ( + TestRoot, _ = ioutil.TempDir("", "test-gitlab-shell") +) + +func TempEnv(env map[string]string) func() { + var original = make(map[string]string) + for key, value := range env { + original[key] = os.Getenv(key) + os.Setenv(key, value) + } + + return func() { + for key, originalValue := range original { + os.Setenv(key, originalValue) + } + } +} + +func PrepareTestRootDir() (func(), error) { + if err := os.MkdirAll(TestRoot, 0700); err != nil { + return nil, err + } + + var oldWd string + cleanup := func() { + if oldWd != "" { + err := os.Chdir(oldWd) + if err != nil { + panic(err) + } + } + + if err := os.RemoveAll(TestRoot); err != nil { + panic(err) + } + } + + if err := copyTestData(); err != nil { + cleanup() + return nil, err + } + + oldWd, err := os.Getwd() + if err != nil { + cleanup() + return nil, err + } + + if err := os.Chdir(TestRoot); err != nil { + cleanup() + return nil, err + } + + return cleanup, nil +} + +func copyTestData() error { + testDataDir, err := getTestDataDir() + if err != nil { + return err + } + + testdata := path.Join(testDataDir, "testroot") + + return copy.Copy(testdata, TestRoot) +} + +func getTestDataDir() (string, error) { + _, currentFile, _, ok := runtime.Caller(0) + if !ok { + return "", fmt.Errorf("Could not get caller info") + } + + return path.Join(path.Dir(currentFile), "testdata"), nil +} + +func Setenv(key, value string) (func(), error) { + oldValue := os.Getenv(key) + err := os.Setenv(key, value) + return func() { os.Setenv(key, oldValue) }, err +} -- cgit v1.2.1