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) + } + } +}