From 4ba384917bedbd3f695961491e7aa8abe8182769 Mon Sep 17 00:00:00 2001 From: lhchavez Date: Sat, 18 Jul 2020 12:36:01 -0700 Subject: [PATCH] Create an endpoint for renaming a repository (#7) This change allows the frontend to rename a repository. This is needed because when creating a problem, the metadata for it needs to be committed into the database before sending out the request to gitserver, and if this happens to fail, there is no way to undo this operation. Instead, now the workflow will be create the repository with a random name, and only if everything is ready to be committed, rename it to the correct name, in a sort of two-phase commit. --- cmd/omegaup-gitserver/auth.go | 2 + cmd/omegaup-gitserver/main.go | 5 +- handler_test.go | 12 ++++ request/request.go | 1 + ziphandler.go | 109 ++++++++++++++++++++++++++++------ ziphandler_test.go | 105 ++++++++++++++++++++++++++++++++ 6 files changed, 215 insertions(+), 19 deletions(-) diff --git a/cmd/omegaup-gitserver/auth.go b/cmd/omegaup-gitserver/auth.go index 0c371e0..acc69e0 100644 --- a/cmd/omegaup-gitserver/auth.go +++ b/cmd/omegaup-gitserver/auth.go @@ -49,6 +49,7 @@ type omegaupAuthorization struct { type authorizationProblemResponse struct { Status string `json:"status"` HasSolved bool `json:"has_solved"` + IsSystem bool `json:"is_system"` IsAdmin bool `json:"is_admin"` CanView bool `json:"can_view"` CanEdit bool `json:"can_edit"` @@ -328,6 +329,7 @@ func (a *omegaupAuthorization) authorize( requestContext.Request.Username = username if username == "omegaup:system" || *insecureSkipAuthorization { // This is the frontend, and we trust it completely. + requestContext.Request.IsSystem = true requestContext.Request.IsAdmin = true requestContext.Request.CanView = true requestContext.Request.CanEdit = true diff --git a/cmd/omegaup-gitserver/main.go b/cmd/omegaup-gitserver/main.go index ae28075..df1bc70 100644 --- a/cmd/omegaup-gitserver/main.go +++ b/cmd/omegaup-gitserver/main.go @@ -87,10 +87,11 @@ func muxHandler( } func (h *muxGitHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - splitPath := strings.SplitN(r.URL.Path[1:], "/", 2) + splitPath := strings.Split(r.URL.Path[1:], "/") if len(splitPath) >= 1 && splitPath[0] == "metrics" { h.metricsHandler.ServeHTTP(w, r) - } else if len(splitPath) == 2 && splitPath[1] == "git-upload-zip" { + } else if len(splitPath) == 2 && splitPath[1] == "git-upload-zip" || + len(splitPath) == 3 && splitPath[1] == "rename-repository" { h.zipHandler.ServeHTTP(w, r) } else { h.gitHandler.ServeHTTP(w, r) diff --git a/handler_test.go b/handler_test.go index 79ba3b1..1f176cf 100644 --- a/handler_test.go +++ b/handler_test.go @@ -32,6 +32,7 @@ const ( userAuthorization = "Basic dXNlcjp1c2Vy" editorAuthorization = "Basic ZWRpdG9yOmVkaXRvcg==" adminAuthorization = "Basic YWRtaW46YWRtaW4=" + systemAuthorization = "OmegaUpSharedSecret secret-token omegaup:system" readonlyAuthorization = "Basic cmVhZG9ubHk6cmVhZG9ubHk=" ) @@ -49,6 +50,17 @@ func authorize( repositoryName string, operation githttp.GitOperation, ) (githttp.AuthorizationLevel, string) { + if r.Header.Get("Authorization") == systemAuthorization { + requestContext := request.FromContext(ctx) + requestContext.Request.Username = "omegaup:system" + requestContext.Request.ProblemName = repositoryName + requestContext.Request.IsSystem = true + requestContext.Request.IsAdmin = true + requestContext.Request.CanView = true + requestContext.Request.CanEdit = true + return githttp.AuthorizationAllowed, "omegaup:system" + } + username, _, ok := r.BasicAuth() if !ok { w.Header().Set("WWW-Authenticate", "Basic realm=\"Git\"") diff --git a/request/request.go b/request/request.go index f0d0b3b..c9ef608 100644 --- a/request/request.go +++ b/request/request.go @@ -17,6 +17,7 @@ type Request struct { ProblemName string Username string Create bool + IsSystem bool IsAdmin bool CanView bool CanEdit bool diff --git a/ziphandler.go b/ziphandler.go index e33774e..672a107 100644 --- a/ziphandler.go +++ b/ziphandler.go @@ -1396,21 +1396,11 @@ type zipUploadHandler struct { log log15.Logger } -func (h *zipUploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { - splitPath := strings.SplitN(r.URL.Path[1:], "/", 2) - if len(splitPath) != 2 { - w.WriteHeader(http.StatusNotFound) - return - } - repositoryName := splitPath[0] - if strings.HasPrefix(repositoryName, ".") { - w.WriteHeader(http.StatusNotFound) - return - } - if splitPath[1] != "git-upload-zip" { - w.WriteHeader(http.StatusNotFound) - return - } +func (h *zipUploadHandler) handleGitUploadZip( + w http.ResponseWriter, + r *http.Request, + repositoryName string, +) { if r.Method != "POST" { w.WriteHeader(http.StatusMethodNotAllowed) return @@ -1484,8 +1474,7 @@ func (h *zipUploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { repositoryPath := path.Join(h.rootPath, repositoryName) h.log.Info( - "Request", - "Method", r.Method, + "git-upload-zip", "path", repositoryPath, "create", requestContext.Request.Create, ) @@ -1624,6 +1613,92 @@ func (h *zipUploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { encoder.Encode(&updateResult) } +func (h *zipUploadHandler) handleRenameRepository( + w http.ResponseWriter, + r *http.Request, + repositoryName string, + targetRepositoryName string, +) { + if r.Method != "GET" { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + ctx := request.NewContext(r.Context(), h.metrics) + + repositoryPath := path.Join(h.rootPath, repositoryName) + targetRepositoryPath := path.Join(h.rootPath, targetRepositoryName) + h.log.Info( + "rename-repository", + "path", repositoryPath, + "target path", targetRepositoryPath, + ) + + level, _ := h.protocol.AuthCallback(ctx, w, r, repositoryName, githttp.OperationPush) + requestContext := request.FromContext(ctx) + if level != githttp.AuthorizationAllowed || !requestContext.Request.IsSystem { + h.log.Error( + "not allowed to rename repository", + "authorization level", level, + "request context", requestContext.Request, + ) + w.WriteHeader(http.StatusForbidden) + return + } + + if err := os.Rename(repositoryPath, targetRepositoryPath); err != nil { + h.log.Error("failed to rename repository", "err", err) + if os.IsNotExist(err) { + } else if os.IsExist(err) { + w.WriteHeader(http.StatusNotFound) + } else if os.IsPermission(err) { + w.WriteHeader(http.StatusForbidden) + } else { + w.WriteHeader(http.StatusInternalServerError) + } + return + } + + h.log.Info( + "rename successful", + "path", repositoryPath, + "target path", targetRepositoryPath, + ) + w.WriteHeader(http.StatusOK) +} + +func (h *zipUploadHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + splitPath := strings.Split(r.URL.Path[1:], "/") + if len(splitPath) < 2 { + w.WriteHeader(http.StatusNotFound) + return + } + repositoryName := splitPath[0] + if strings.HasPrefix(repositoryName, ".") { + w.WriteHeader(http.StatusNotFound) + return + } + + if splitPath[1] == "git-upload-zip" { + if len(splitPath) != 2 { + w.WriteHeader(http.StatusNotFound) + return + } + h.handleGitUploadZip(w, r, repositoryName) + return + } + if splitPath[1] == "rename-repository" { + if len(splitPath) != 3 { + w.WriteHeader(http.StatusNotFound) + return + } + h.handleRenameRepository(w, r, repositoryName, splitPath[2]) + return + } + h.log.Error("failed to rename repository", "split path", splitPath) + w.WriteHeader(http.StatusNotFound) +} + // ZipHandler is the HTTP handler that allows uploading .zip files. func ZipHandler( rootPath string, diff --git a/ziphandler_test.go b/ziphandler_test.go index 7288c20..255713d 100644 --- a/ziphandler_test.go +++ b/ziphandler_test.go @@ -720,3 +720,108 @@ func TestUpdateProblemSettingsWithCustomValidator(t *testing.T) { } } } + +func TestRenameProblem(t *testing.T) { + tmpDir, err := ioutil.TempDir("", t.Name()) + if err != nil { + t.Fatalf("Failed to create directory: %v", err) + } + if os.Getenv("PRESERVE") == "" { + defer os.RemoveAll(tmpDir) + } + + log := base.StderrLog() + ts := httptest.NewServer(ZipHandler( + tmpDir, + NewGitProtocol(authorize, nil, true, OverallWallTimeHardLimit, fakeInteractiveSettingsCompiler, log), + &base.NoOpMetrics{}, + log, + )) + defer ts.Close() + + problemAlias := "sumas-validator" + + // Create the problem. + { + zipContents, err := gitservertest.CreateZip( + map[string]io.Reader{ + "settings.json": strings.NewReader(gitservertest.CustomValidatorSettingsJSON), + "cases/0.in": strings.NewReader("1 2\n"), + "cases/0.out": strings.NewReader("3\n"), + "statements/es.markdown": strings.NewReader("Sumaz\n"), + "validator.py": strings.NewReader("print 1\n"), + }, + ) + if err != nil { + t.Fatalf("Failed to create zip: %v", err) + } + postZip( + t, + adminAuthorization, + problemAlias, + nil, + ZipMergeStrategyTheirs, + zipContents, + "initial commit", + true, // create + true, // useMultipartFormData + ts, + ) + if _, err := os.Stat(path.Join(tmpDir, problemAlias)); err != nil { + t.Fatalf("Stat on old problem repository failed: %v", err) + } + } + + // Rename the problem using an admin user. + { + renameURL, err := url.Parse(ts.URL + "/" + problemAlias + "/rename-repository/renamed") + if err != nil { + t.Fatalf("Failed to parse URL: %v", err) + } + req := &http.Request{ + URL: renameURL, + Method: "GET", + Header: map[string][]string{ + "Authorization": {adminAuthorization}, + }, + } + res, err := ts.Client().Do(req) + if err != nil { + t.Fatalf("Failed to rename problem: %v", err) + } + defer res.Body.Close() + if http.StatusForbidden != res.StatusCode { + t.Fatalf("Unexpected result: expected %v, got %v", http.StatusForbidden, res.StatusCode) + } + } + + // Rename the problem using the system user. + { + renameURL, err := url.Parse(ts.URL + "/" + problemAlias + "/rename-repository/renamed") + if err != nil { + t.Fatalf("Failed to parse URL: %v", err) + } + req := &http.Request{ + URL: renameURL, + Method: "GET", + Header: map[string][]string{ + "Authorization": {systemAuthorization}, + }, + } + res, err := ts.Client().Do(req) + if err != nil { + t.Fatalf("Failed to rename problem: %v", err) + } + defer res.Body.Close() + if res.StatusCode != http.StatusOK { + t.Fatalf("Failed to rename problem: Status %v, headers: %v", res.StatusCode, res.Header) + } + + if _, err := os.Stat(path.Join(tmpDir, problemAlias)); !os.IsNotExist(err) { + t.Fatalf("Stat on old problem repository failed: %v", err) + } + if _, err := os.Stat(path.Join(tmpDir, "renamed")); err != nil { + t.Fatalf("Stat on new problem repository failed: %v", err) + } + } +}