diff options
author | Paul Okstad <pokstad@gitlab.com> | 2019-11-01 18:34:57 +0300 |
---|---|---|
committer | Paul Okstad <pokstad@gitlab.com> | 2019-11-01 18:34:57 +0300 |
commit | 2d465eb4419d862f34f2720381ec6ec50b93842a (patch) | |
tree | 9a9dff30197376c4120f252cadcd079f05987277 | |
parent | c8d2fa42a571bec9ff225b9a65c98fa47ca0dba7 (diff) | |
parent | a5e50b36a28e279adae2d5247fe985c9bb2a9945 (diff) |
Merge branch 'jc-check-subcmd' into 'master'
Add check subcommand in gitaly-hooks
Closes #2087
See merge request gitlab-org/gitaly!1587
-rw-r--r-- | changelogs/unreleased/jc-check-subcmd.yml | 5 | ||||
-rw-r--r-- | cmd/gitaly-hooks/hooks.go | 77 | ||||
-rw-r--r-- | cmd/gitaly-hooks/hooks_test.go | 106 |
3 files changed, 165 insertions, 23 deletions
diff --git a/changelogs/unreleased/jc-check-subcmd.yml b/changelogs/unreleased/jc-check-subcmd.yml new file mode 100644 index 000000000..be33648e8 --- /dev/null +++ b/changelogs/unreleased/jc-check-subcmd.yml @@ -0,0 +1,5 @@ +--- +title: Add check subcommand in gitaly-hooks +merge_request: 1587 +author: +type: added diff --git a/cmd/gitaly-hooks/hooks.go b/cmd/gitaly-hooks/hooks.go index c699d0c6d..c526c1033 100644 --- a/cmd/gitaly-hooks/hooks.go +++ b/cmd/gitaly-hooks/hooks.go @@ -1,16 +1,18 @@ package main import ( - "bytes" "context" "errors" - "io" + "fmt" + "net/http" "os" "os/exec" "path/filepath" + "strings" "gitlab.com/gitlab-org/gitaly/internal/command" "gitlab.com/gitlab-org/gitaly/internal/log" + "gopkg.in/yaml.v2" ) func main() { @@ -20,20 +22,33 @@ func main() { logger.Fatal(errors.New("requires hook name")) } + subCmd := os.Args[1] + + if subCmd == "check" { + configPath := os.Args[2] + + if err := checkGitlabAccess(configPath); err != nil { + os.Stderr.WriteString(err.Error()) + os.Exit(1) + } + + os.Stdout.WriteString("OK") + os.Exit(0) + } + gitlabRubyDir := os.Getenv("GITALY_RUBY_DIR") if gitlabRubyDir == "" { logger.Fatal(errors.New("GITALY_RUBY_DIR not set")) } - hookName := os.Args[1] - rubyHookPath := filepath.Join(gitlabRubyDir, "gitlab-shell", "hooks", hookName) + rubyHookPath := filepath.Join(gitlabRubyDir, "gitlab-shell", "hooks", subCmd) ctx, cancel := context.WithCancel(context.Background()) defer cancel() var hookCmd *exec.Cmd - switch hookName { + switch subCmd { case "update": args := os.Args[2:] if len(args) != 3 { @@ -43,14 +58,12 @@ func main() { hookCmd = exec.Command(rubyHookPath, args...) case "pre-receive", "post-receive": hookCmd = exec.Command(rubyHookPath) + default: logger.Fatal(errors.New("hook name invalid")) } - var stderr bytes.Buffer - mw := io.MultiWriter(&stderr, os.Stderr) - - cmd, err := command.New(ctx, hookCmd, os.Stdin, os.Stdout, mw, os.Environ()...) + cmd, err := command.New(ctx, hookCmd, os.Stdin, os.Stdout, os.Stderr, os.Environ()...) if err != nil { logger.Fatalf("error when starting command for %v: %v", rubyHookPath, err) } @@ -59,3 +72,49 @@ func main() { os.Exit(1) } } + +// GitlabShellConfig contains a subset of gitlabshell's config.yml +type GitlabShellConfig struct { + GitlabURL string `yaml:"gitlab_url"` + HTTPSettings HTTPSettings `yaml:"http_settings"` +} + +// HTTPSettings contains fields for http settings +type HTTPSettings struct { + User string `yaml:"user"` + Password string `yaml:"password"` +} + +func checkGitlabAccess(configPath string) error { + cfgFile, err := os.Open(configPath) + if err != nil { + return fmt.Errorf("error when opening config file: %v", err) + } + defer cfgFile.Close() + + config := GitlabShellConfig{} + + if err := yaml.NewDecoder(cfgFile).Decode(&config); err != nil { + return fmt.Errorf("load toml: %v", err) + } + + req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/v4/internal/check", strings.TrimRight(config.GitlabURL, "/")), nil) + if err != nil { + return fmt.Errorf("could not create request for %s: %v", config.GitlabURL, err) + } + + req.SetBasicAuth(config.HTTPSettings.User, config.HTTPSettings.Password) + + client := &http.Client{} + + resp, err := client.Do(req) + if err != nil { + return fmt.Errorf("error with request for %s: %v", config.GitlabURL, err) + } + + if resp.StatusCode != 200 { + return fmt.Errorf("FAILED. code: %d", resp.StatusCode) + } + + return nil +} diff --git a/cmd/gitaly-hooks/hooks_test.go b/cmd/gitaly-hooks/hooks_test.go index cf8990c20..c7dbae128 100644 --- a/cmd/gitaly-hooks/hooks_test.go +++ b/cmd/gitaly-hooks/hooks_test.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "encoding/base64" "encoding/json" "fmt" "io/ioutil" @@ -13,6 +14,7 @@ import ( "path" "path/filepath" "strconv" + "strings" "testing" "github.com/stretchr/testify/require" @@ -44,10 +46,10 @@ func TestHooksPrePostReceive(t *testing.T) { changes := "abc" - ts := gitlabTestServer(t, secretToken, key, glRepository, changes, true) + ts := gitlabTestServer(t, "", "", secretToken, key, glRepository, changes, true) defer ts.Close() - writeTemporaryConfigFile(t, tempGitlabShellDir, ts.URL) + writeTemporaryConfigFile(t, tempGitlabShellDir, GitlabShellConfig{GitlabURL: ts.URL}) writeShellSecretFile(t, tempGitlabShellDir, secretToken) for _, hook := range []string{"pre-receive", "post-receive"} { @@ -76,7 +78,7 @@ func TestHooksUpdate(t *testing.T) { tempGitlabShellDir, cleanup := createTempGitlabShellDir(t) defer cleanup() - writeTemporaryConfigFile(t, tempGitlabShellDir, "http://www.example.com") + writeTemporaryConfigFile(t, tempGitlabShellDir, GitlabShellConfig{GitlabURL: "http://www.example.com"}) writeShellSecretFile(t, tempGitlabShellDir, "the wrong token") require.NoError(t, os.MkdirAll(filepath.Join(tempGitlabShellDir, "hooks", "update.d"), 0755)) @@ -120,10 +122,10 @@ func TestHooksPostReceiveFailed(t *testing.T) { // send back {"reference_counter_increased": false}, indicating something went wrong // with the call - ts := gitlabTestServer(t, secretToken, key, glRepository, "", false) + ts := gitlabTestServer(t, "", "", secretToken, key, glRepository, "", false) defer ts.Close() - writeTemporaryConfigFile(t, tempGitlabShellDir, ts.URL) + writeTemporaryConfigFile(t, tempGitlabShellDir, GitlabShellConfig{GitlabURL: ts.URL}) writeShellSecretFile(t, tempGitlabShellDir, secretToken) for envName, env := range map[string][]string{"new": env(t, glRepository, tempGitlabShellDir, key), "old": oldEnv(t, glRepository, tempGitlabShellDir, key)} { @@ -154,10 +156,10 @@ func TestHooksNotAllowed(t *testing.T) { tempGitlabShellDir, cleanup := createTempGitlabShellDir(t) defer cleanup() - ts := gitlabTestServer(t, secretToken, key, glRepository, "", true) + ts := gitlabTestServer(t, "", "", secretToken, key, glRepository, "", true) defer ts.Close() - writeTemporaryConfigFile(t, tempGitlabShellDir, ts.URL) + writeTemporaryConfigFile(t, tempGitlabShellDir, GitlabShellConfig{GitlabURL: ts.URL}) writeShellSecretFile(t, tempGitlabShellDir, "the wrong token") for envName, env := range map[string][]string{"new": env(t, glRepository, tempGitlabShellDir, key), "old": oldEnv(t, glRepository, tempGitlabShellDir, key)} { @@ -176,8 +178,54 @@ func TestHooksNotAllowed(t *testing.T) { } } -type GitlabShellConfig struct { - GitlabURL string `yaml:"gitlab_url"` +func TestCheckOK(t *testing.T) { + user, password := "user123", "password321" + + ts := gitlabTestServer(t, user, password, "", 0, "", "", false) + defer ts.Close() + + tempDir, err := ioutil.TempDir("", t.Name()) + require.NoError(t, err) + defer func() { + os.RemoveAll(tempDir) + }() + + configPath := writeTemporaryConfigFile(t, tempDir, GitlabShellConfig{GitlabURL: ts.URL, HTTPSettings: HTTPSettings{User: user, Password: password}}) + + cmd := exec.Command(fmt.Sprintf("%s/gitaly-hooks", config.Config.BinDir), "check", configPath) + + var stderr, stdout bytes.Buffer + cmd.Stderr = &stderr + cmd.Stdout = &stdout + + require.NoError(t, cmd.Run()) + require.Empty(t, stderr.String()) + require.Equal(t, "OK", stdout.String()) +} + +func TestCheckBadCreds(t *testing.T) { + user, password := "user123", "password321" + + ts := gitlabTestServer(t, user, password, "", 0, "", "", false) + defer ts.Close() + + tempDir, err := ioutil.TempDir("", t.Name()) + require.NoError(t, err) + defer func() { + os.RemoveAll(tempDir) + }() + + configPath := writeTemporaryConfigFile(t, tempDir, GitlabShellConfig{GitlabURL: ts.URL, HTTPSettings: HTTPSettings{User: user + "wrong", Password: password}}) + + cmd := exec.Command(fmt.Sprintf("%s/gitaly-hooks", config.Config.BinDir), "check", configPath) + + var stderr, stdout bytes.Buffer + cmd.Stderr = &stderr + cmd.Stdout = &stdout + + require.Error(t, cmd.Run()) + require.Equal(t, "FAILED. code: 401", stderr.String()) + require.Empty(t, stdout.String()) } func handleAllowed(t *testing.T, secretToken string, key int, glRepository, changes string) func(w http.ResponseWriter, r *http.Request) { @@ -230,11 +278,38 @@ func handlePostReceive(t *testing.T, secretToken string, key int, glRepository, } } -func gitlabTestServer(t *testing.T, secretToken string, key int, glRepository, changes string, postReceiveCounterDecreased bool) *httptest.Server { +func handleCheck(t *testing.T, user, password string) func(w http.ResponseWriter, r *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + auth := strings.SplitN(r.Header.Get("Authorization"), " ", 2) + + if len(auth) != 2 || auth[0] != "Basic" { + http.Error(w, "authorization failed", http.StatusUnauthorized) + return + } + + payload, _ := base64.StdEncoding.DecodeString(auth[1]) + pair := strings.SplitN(string(payload), ":", 2) + + if pair[0] != user || pair[1] != password { + w.WriteHeader(http.StatusUnauthorized) + return + } + + w.WriteHeader(http.StatusOK) + } +} + +func gitlabTestServer(t *testing.T, + user, password, secretToken string, + key int, + glRepository, + changes string, + postReceiveCounterDecreased bool) *httptest.Server { mux := http.NewServeMux() mux.Handle("/api/v4/internal/allowed", http.HandlerFunc(handleAllowed(t, secretToken, key, glRepository, changes))) mux.Handle("/api/v4/internal/pre_receive", http.HandlerFunc(handlePreReceive(t, secretToken, glRepository))) mux.Handle("/api/v4/internal/post_receive", http.HandlerFunc(handlePostReceive(t, secretToken, key, glRepository, changes, postReceiveCounterDecreased))) + mux.Handle("/api/v4/internal/check", http.HandlerFunc(handleCheck(t, user, password))) return httptest.NewServer(mux) } @@ -247,11 +322,14 @@ func createTempGitlabShellDir(t *testing.T) (string, func()) { } } -func writeTemporaryConfigFile(t *testing.T, dir, testServerURL string) { - cfg := GitlabShellConfig{GitlabURL: testServerURL} - out, err := yaml.Marshal(cfg) +func writeTemporaryConfigFile(t *testing.T, dir string, config GitlabShellConfig) string { + out, err := yaml.Marshal(&config) require.NoError(t, err) - require.NoError(t, ioutil.WriteFile(filepath.Join(dir, "config.yml"), out, 0644)) + + path := filepath.Join(dir, "config.yml") + require.NoError(t, ioutil.WriteFile(path, out, 0644)) + + return path } func env(t *testing.T, glRepo, gitlabShellDir string, key int) []string { |