Просмотр исходного кода

Merge pull request #38 from overleaf/msw-disabled-projects

Report write-latex API 4xx errors in a friendly way to users
Michael Walker 8 лет назад
Родитель
Сommit
3c86eb0d52

+ 5 - 2
services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/Bridge.java

@@ -35,6 +35,7 @@ import uk.ac.ic.wlgitbridge.git.handler.hook.WriteLatexPutHook;
 import uk.ac.ic.wlgitbridge.server.FileHandler;
 import uk.ac.ic.wlgitbridge.server.PostbackContents;
 import uk.ac.ic.wlgitbridge.server.PostbackHandler;
+import uk.ac.ic.wlgitbridge.snapshot.base.MissingRepositoryException;
 import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException;
 import uk.ac.ic.wlgitbridge.snapshot.getforversion.SnapshotAttachment;
 import uk.ac.ic.wlgitbridge.snapshot.push.PostbackManager;
@@ -383,6 +384,7 @@ public class Bridge {
      * @param hostname
      * @throws SnapshotPostException
      * @throws IOException
+     * @throws MissingRepositoryException
      * @throws ForbiddenException
      */
     public void push(
@@ -391,7 +393,7 @@ public class Bridge {
             RawDirectory directoryContents,
             RawDirectory oldDirectoryContents,
             String hostname
-    ) throws SnapshotPostException, IOException, ForbiddenException {
+    ) throws SnapshotPostException, IOException, MissingRepositoryException, ForbiddenException {
         try (LockGuard __ = lock.lockGuard(projectName)) {
             pushCritical(
                     oauth2,
@@ -460,6 +462,7 @@ public class Bridge {
      * @param directoryContents
      * @param oldDirectoryContents
      * @throws IOException
+     * @throws MissingRepositoryException
      * @throws ForbiddenException
      * @throws SnapshotPostException
      */
@@ -468,7 +471,7 @@ public class Bridge {
             String projectName,
             RawDirectory directoryContents,
             RawDirectory oldDirectoryContents
-    ) throws IOException, ForbiddenException, SnapshotPostException {
+    ) throws IOException, MissingRepositoryException, ForbiddenException, SnapshotPostException {
         Log.info("[{}] Pushing", projectName);
         String postbackKey = postbackManager.makeKeyForProject(projectName);
         Log.info(

+ 4 - 2
services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/snapshot/SnapshotApi.java

@@ -2,6 +2,7 @@ package uk.ac.ic.wlgitbridge.bridge.snapshot;
 
 import com.google.api.client.auth.oauth2.Credential;
 import uk.ac.ic.wlgitbridge.data.CandidateSnapshot;
+import uk.ac.ic.wlgitbridge.snapshot.base.MissingRepositoryException;
 import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException;
 import uk.ac.ic.wlgitbridge.snapshot.exception.FailedConnectionException;
 import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocResult;
@@ -33,13 +34,14 @@ public interface SnapshotApi {
             String postbackKey);
 
     static <T> T getResult(CompletableFuture<T> result)
-            throws FailedConnectionException, ForbiddenException {
+            throws MissingRepositoryException, FailedConnectionException, ForbiddenException {
         try {
             return result.join();
         } catch (CompletionException e) {
             try {
                 throw e.getCause();
-            } catch (FailedConnectionException
+            } catch (MissingRepositoryException
+                    | FailedConnectionException
                     | ForbiddenException
                     | RuntimeException r) {
                 throw r;

+ 2 - 1
services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/snapshot/SnapshotApiFacade.java

@@ -4,6 +4,7 @@ import com.google.api.client.auth.oauth2.Credential;
 import uk.ac.ic.wlgitbridge.data.CandidateSnapshot;
 import uk.ac.ic.wlgitbridge.data.model.Snapshot;
 import uk.ac.ic.wlgitbridge.git.exception.GitUserException;
+import uk.ac.ic.wlgitbridge.snapshot.base.MissingRepositoryException;
 import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException;
 import uk.ac.ic.wlgitbridge.snapshot.exception.FailedConnectionException;
 import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocResult;
@@ -65,7 +66,7 @@ public class SnapshotApiFacade {
             Optional<Credential> oauth2,
             CandidateSnapshot candidateSnapshot,
             String postbackKey
-    ) throws FailedConnectionException, ForbiddenException {
+    ) throws MissingRepositoryException, FailedConnectionException, ForbiddenException {
         return SnapshotApi.getResult(api.push(
                 oauth2, candidateSnapshot, postbackKey));
     }

+ 26 - 4
services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/Oauth2Filter.java

@@ -6,6 +6,7 @@ import org.apache.commons.codec.binary.Base64;
 import org.eclipse.jetty.server.Request;
 import uk.ac.ic.wlgitbridge.application.config.Oauth2;
 import uk.ac.ic.wlgitbridge.bridge.snapshot.SnapshotApi;
+import uk.ac.ic.wlgitbridge.snapshot.base.MissingRepositoryException;
 import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException;
 import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocRequest;
 import uk.ac.ic.wlgitbridge.util.Instance;
@@ -76,6 +77,8 @@ public class Oauth2Filter implements Filter {
                     filterChain
             );
             return;
+        } catch (MissingRepositoryException e) {
+            handleMissingRepository(project, e, (HttpServletResponse) servletResponse);
         }
         Log.info("[{}] Auth not needed", project);
         filterChain.doFilter(servletRequest, servletResponse);
@@ -130,7 +133,7 @@ public class Oauth2Filter implements Filter {
                                         )
                                 ).execute().getAccessToken();
                             } catch (TokenResponseException e) {
-                                unauthorized(projectName, capturedUsername, e.getStatusCode(), request, response);
+                                handleNeedAuthorization(projectName, capturedUsername, e.getStatusCode(), request, response);
                                 return;
                             }
                             final Credential cred = new Credential.Builder(
@@ -145,7 +148,7 @@ public class Oauth2Filter implements Filter {
                                     servletResponse
                             );
                         } else {
-                            unauthorized(projectName, capturedUsername, 0, request, response);
+                            handleNeedAuthorization(projectName, capturedUsername, 0, request, response);
                         }
                     } catch (UnsupportedEncodingException e) {
                         throw new Error("Couldn't retrieve authentication", e);
@@ -153,14 +156,14 @@ public class Oauth2Filter implements Filter {
                 }
             }
         } else {
-            unauthorized(projectName, capturedUsername, 0, request, response);
+            handleNeedAuthorization(projectName, capturedUsername, 0, request, response);
         }
     }
 
     @Override
     public void destroy() {}
 
-    private void unauthorized(
+    private void handleNeedAuthorization(
             String projectName,
             String userName,
             int statusCode,
@@ -200,4 +203,23 @@ public class Oauth2Filter implements Filter {
         w.close();
     }
 
+    private void handleMissingRepository(
+            String projectName,
+            MissingRepositoryException e,
+            HttpServletResponse response
+    ) throws IOException {
+        Log.info("[{}] Project missing.", projectName);
+
+        response.setContentType("text/plain");
+
+        // git special-cases 404 to give "repository '%s' not found",
+        // rather than displaying the raw status code.
+        response.setStatus(404);
+
+        PrintWriter w = response.getWriter();
+        for (String line : e.getDescriptionLines()) {
+            w.println(line);
+        }
+        w.close();
+    }
 }

+ 47 - 0
services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/snapshot/base/MissingRepositoryException.java

@@ -0,0 +1,47 @@
+package uk.ac.ic.wlgitbridge.snapshot.base;
+
+import com.google.gson.JsonElement;
+import uk.ac.ic.wlgitbridge.git.exception.SnapshotAPIException;
+
+import java.util.Arrays;
+import java.util.ArrayList;
+import java.util.List;
+
+public class MissingRepositoryException extends SnapshotAPIException {
+
+    public static final List<String> GENERIC_REASON = Arrays.asList(
+        "This Overleaf project currently has no git access.",
+        "",
+        "If this problem persists, please contact us."
+    );
+
+    public static final List<String> EXPORTED_TO_V2 = Arrays.asList(
+        "This Overleaf project has been moved to Overleaf v2, and git access is temporarily unsupported.",
+        "",
+        "See https://www.overleaf.com/help/342 for more information."
+    );
+
+    private List<String> descriptionLines;
+
+    public MissingRepositoryException() {
+        descriptionLines = new ArrayList<String>();
+    }
+
+    public MissingRepositoryException(List<String> descriptionLines) {
+        this.descriptionLines = descriptionLines;
+    }
+
+    @Override
+    public void fromJSON(JsonElement json) {}
+
+    @Override
+    public String getMessage() {
+        return "no git access";
+    }
+
+    @Override
+    public List<String> getDescriptionLines() {
+        return this.descriptionLines;
+    }
+
+}

+ 26 - 7
services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/snapshot/base/Request.java

@@ -2,6 +2,7 @@ package uk.ac.ic.wlgitbridge.snapshot.base;
 
 import com.google.api.client.http.*;
 import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
 import com.ning.http.client.AsyncHttpClient;
 import uk.ac.ic.wlgitbridge.snapshot.exception.FailedConnectionException;
 import uk.ac.ic.wlgitbridge.util.Instance;
@@ -50,7 +51,7 @@ public abstract class Request<T extends Result> {
         return ret;
     }
 
-    private T getResult() throws FailedConnectionException, ForbiddenException {
+    private T getResult() throws MissingRepositoryException, FailedConnectionException, ForbiddenException {
         try {
             HttpResponse response = future.get();
             Log.info(
@@ -68,12 +69,30 @@ public abstract class Request<T extends Result> {
             throw new FailedConnectionException();
         } catch (ExecutionException e) {
             Throwable cause = e.getCause();
-            if (cause instanceof HttpResponseException &&
-                    (((HttpResponseException) cause).getStatusCode() ==
-                            HttpServletResponse.SC_UNAUTHORIZED ||
-                    ((HttpResponseException) cause).getStatusCode() ==
-                            HttpServletResponse.SC_FORBIDDEN)) {
-                throw new ForbiddenException();
+            if (cause instanceof HttpResponseException) {
+                HttpResponseException httpCause = (HttpResponseException) cause;
+                int sc = httpCause.getStatusCode();
+                if (sc == HttpServletResponse.SC_UNAUTHORIZED || sc == HttpServletResponse.SC_FORBIDDEN) {
+                    throw new ForbiddenException();
+                } else if (sc == HttpServletResponse.SC_NOT_FOUND) {
+                    try {
+                        JsonObject json = Instance.gson.fromJson(httpCause.getContent(), JsonObject.class);
+                        String message = json.get("message").getAsString();
+
+                        if ("Exported to v2".equals(message)) {
+                            throw new MissingRepositoryException(MissingRepositoryException.EXPORTED_TO_V2);
+                        }
+                    } catch (IllegalStateException
+                            | ClassCastException
+                            | NullPointerException _) {
+                        // disregard any errors that arose while handling the JSON
+                    }
+
+                    throw new MissingRepositoryException();
+                } else if (sc >= 400 && sc < 500) {
+                    throw new MissingRepositoryException(MissingRepositoryException.GENERIC_REASON);
+                }
+                throw new FailedConnectionException(cause);
             } else {
                 throw new FailedConnectionException(cause);
             }

+ 62 - 2
services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/WLGitBridgeIntegrationTest.java

@@ -42,8 +42,14 @@ public class WLGitBridgeIntegrationTest {
         put("canCloneMultipleRepositories", new HashMap<String, SnapshotAPIState>() {{
             put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/canCloneMultipleRepositories/state/state.json")).build());
         }});
-        put("cannotCloneAProtectedProject", new HashMap<String, SnapshotAPIState>() {{
-            put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/cannotCloneAProtectedProject/state/state.json")).build());
+        put("cannotCloneAProtectedProjectWithoutAuthentication", new HashMap<String, SnapshotAPIState>() {{
+            put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/cannotCloneAProtectedProjectWithoutAuthentication/state/state.json")).build());
+        }});
+        put("cannotCloneA4xxProject", new HashMap<String, SnapshotAPIState>() {{
+            put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/cannotCloneA4xxProject/state/state.json")).build());
+        }});
+        put("cannotCloneAMissingProject", new HashMap<String, SnapshotAPIState>() {{
+            put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/cannotCloneAMissingProject/state/state.json")).build());
         }});
         put("canPullAModifiedTexFile", new HashMap<String, SnapshotAPIState>() {{
             put("base", new SnapshotAPIStateBuilder(getResourceAsStream("/canPullAModifiedTexFile/base/state.json")).build());
@@ -727,6 +733,60 @@ public class WLGitBridgeIntegrationTest {
         wlgb.stop();
     }
 
+    @Test
+    public void cannotCloneAProtectedProjectWithoutAuthentication() throws IOException, GitAPIException, InterruptedException {
+        int gitBridgePort = 33883;
+        int mockServerPort = 3883;
+
+        MockSnapshotServer server = new MockSnapshotServer(mockServerPort, getResource("/cannotCloneAProtectedProjectWithoutAuthentication").toFile());
+        server.start();
+        server.setState(states.get("cannotCloneAProtectedProjectWithoutAuthentication").get("state"));
+        GitBridgeApp wlgb = new GitBridgeApp(new String[] {
+            makeConfigFile(gitBridgePort, mockServerPort)
+        });
+
+        wlgb.run();
+        Process gitProcess = runtime.exec("git clone http://127.0.0.1:" + gitBridgePort + "/testproj.git", null, dir);
+        wlgb.stop();
+        assertNotEquals(0, gitProcess.waitFor());
+    }
+
+    @Test
+    public void cannotCloneA4xxProject() throws IOException, GitAPIException, InterruptedException {
+        int gitBridgePort = 33879;
+        int mockServerPort = 3879;
+
+        MockSnapshotServer server = new MockSnapshotServer(mockServerPort, getResource("/cannotCloneA4xxProject").toFile());
+        server.start();
+        server.setState(states.get("cannotCloneA4xxProject").get("state"));
+        GitBridgeApp wlgb = new GitBridgeApp(new String[] {
+            makeConfigFile(gitBridgePort, mockServerPort)
+        });
+
+        wlgb.run();
+        Process gitProcess = runtime.exec("git clone http://127.0.0.1:" + gitBridgePort + "/testproj.git", null, dir);
+        wlgb.stop();
+        assertNotEquals(0, gitProcess.waitFor());
+    }
+
+    @Test
+    public void cannotCloneAMissingProject() throws IOException, GitAPIException, InterruptedException {
+        int gitBridgePort = 33880;
+        int mockServerPort = 3880;
+
+        MockSnapshotServer server = new MockSnapshotServer(mockServerPort, getResource("/cannotCloneAMissingProject").toFile());
+        server.start();
+        server.setState(states.get("cannotCloneAMissingProject").get("state"));
+        GitBridgeApp wlgb = new GitBridgeApp(new String[] {
+            makeConfigFile(gitBridgePort, mockServerPort)
+        });
+
+        wlgb.run();
+        Process gitProcess = runtime.exec("git clone http://127.0.0.1:" + gitBridgePort + "/testproj.git", null, dir);
+        wlgb.stop();
+        assertNotEquals(0, gitProcess.waitFor());
+    }
+
     private String makeConfigFile(
             int port,
             int apiPort

+ 18 - 0
services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/cannotCloneA4xxProject/state/state.json

@@ -0,0 +1,18 @@
+[
+    {
+	"project": "gone",
+	"getDoc": {
+            "error": 410,
+            "versionID": 1,
+            "createdAt": "2018-02-05T15:30:00Z",
+            "email": "michael.walker@overleaf.com",
+            "name": "msw"
+	},
+	"getSavedVers": [],
+	"getForVers": [],
+	"push": "success",
+	"postback": {
+	    "type": "outOfDate"
+	}
+    }
+]

+ 18 - 0
services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/cannotCloneAMissingProject/state/state.json

@@ -0,0 +1,18 @@
+[
+    {
+	"project": "missing",
+	"getDoc": {
+            "error": 404,
+            "versionID": 1,
+            "createdAt": "2018-02-06T13:29:00Z",
+            "email": "michael.walker@overleaf.com",
+            "name": "msw"
+	},
+	"getSavedVers": [],
+	"getForVers": [],
+	"push": "success",
+	"postback": {
+	    "type": "outOfDate"
+	}
+    }
+]

+ 0 - 0
services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/cannotCloneAProtectedProject/state/state.json → services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/cannotCloneAProtectedProjectWithoutAuthentication/state/state.json