Skip to content

Commit

Permalink
Create an endpoint for renaming a repository (#7)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
lhchavez committed Jul 18, 2020
1 parent e2a3a91 commit 4ba3849
Show file tree
Hide file tree
Showing 6 changed files with 215 additions and 19 deletions.
2 changes: 2 additions & 0 deletions cmd/omegaup-gitserver/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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
Expand Down
5 changes: 3 additions & 2 deletions cmd/omegaup-gitserver/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
12 changes: 12 additions & 0 deletions handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ const (
userAuthorization = "Basic dXNlcjp1c2Vy"
editorAuthorization = "Basic ZWRpdG9yOmVkaXRvcg=="
adminAuthorization = "Basic YWRtaW46YWRtaW4="
systemAuthorization = "OmegaUpSharedSecret secret-token omegaup:system"
readonlyAuthorization = "Basic cmVhZG9ubHk6cmVhZG9ubHk="
)

Expand All @@ -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\"")
Expand Down
1 change: 1 addition & 0 deletions request/request.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ type Request struct {
ProblemName string
Username string
Create bool
IsSystem bool
IsAdmin bool
CanView bool
CanEdit bool
Expand Down
109 changes: 92 additions & 17 deletions ziphandler.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
)
Expand Down Expand Up @@ -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,
Expand Down
105 changes: 105 additions & 0 deletions ziphandler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
}

0 comments on commit 4ba3849

Please sign in to comment.