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

Report write-latex API 4xx errors in a friendly way to users
This commit is contained in:
Michael Walker 2018-02-07 15:03:19 +00:00 committed by GitHub
commit 3c86eb0d52
10 changed files with 208 additions and 18 deletions

View file

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

View file

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

View file

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

View file

@ -6,6 +6,7 @@ import org.apache.commons.codec.binary.Base64;
import org.eclipse.jetty.server.Request; import org.eclipse.jetty.server.Request;
import uk.ac.ic.wlgitbridge.application.config.Oauth2; import uk.ac.ic.wlgitbridge.application.config.Oauth2;
import uk.ac.ic.wlgitbridge.bridge.snapshot.SnapshotApi; 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.base.ForbiddenException;
import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocRequest; import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocRequest;
import uk.ac.ic.wlgitbridge.util.Instance; import uk.ac.ic.wlgitbridge.util.Instance;
@ -76,6 +77,8 @@ public class Oauth2Filter implements Filter {
filterChain filterChain
); );
return; return;
} catch (MissingRepositoryException e) {
handleMissingRepository(project, e, (HttpServletResponse) servletResponse);
} }
Log.info("[{}] Auth not needed", project); Log.info("[{}] Auth not needed", project);
filterChain.doFilter(servletRequest, servletResponse); filterChain.doFilter(servletRequest, servletResponse);
@ -130,7 +133,7 @@ public class Oauth2Filter implements Filter {
) )
).execute().getAccessToken(); ).execute().getAccessToken();
} catch (TokenResponseException e) { } catch (TokenResponseException e) {
unauthorized(projectName, capturedUsername, e.getStatusCode(), request, response); handleNeedAuthorization(projectName, capturedUsername, e.getStatusCode(), request, response);
return; return;
} }
final Credential cred = new Credential.Builder( final Credential cred = new Credential.Builder(
@ -145,7 +148,7 @@ public class Oauth2Filter implements Filter {
servletResponse servletResponse
); );
} else { } else {
unauthorized(projectName, capturedUsername, 0, request, response); handleNeedAuthorization(projectName, capturedUsername, 0, request, response);
} }
} catch (UnsupportedEncodingException e) { } catch (UnsupportedEncodingException e) {
throw new Error("Couldn't retrieve authentication", e); throw new Error("Couldn't retrieve authentication", e);
@ -153,14 +156,14 @@ public class Oauth2Filter implements Filter {
} }
} }
} else { } else {
unauthorized(projectName, capturedUsername, 0, request, response); handleNeedAuthorization(projectName, capturedUsername, 0, request, response);
} }
} }
@Override @Override
public void destroy() {} public void destroy() {}
private void unauthorized( private void handleNeedAuthorization(
String projectName, String projectName,
String userName, String userName,
int statusCode, int statusCode,
@ -200,4 +203,23 @@ public class Oauth2Filter implements Filter {
w.close(); 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();
}
} }

View file

@ -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;
}
}

View file

@ -2,6 +2,7 @@ package uk.ac.ic.wlgitbridge.snapshot.base;
import com.google.api.client.http.*; import com.google.api.client.http.*;
import com.google.gson.JsonElement; import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.ning.http.client.AsyncHttpClient; import com.ning.http.client.AsyncHttpClient;
import uk.ac.ic.wlgitbridge.snapshot.exception.FailedConnectionException; import uk.ac.ic.wlgitbridge.snapshot.exception.FailedConnectionException;
import uk.ac.ic.wlgitbridge.util.Instance; import uk.ac.ic.wlgitbridge.util.Instance;
@ -50,7 +51,7 @@ public abstract class Request<T extends Result> {
return ret; return ret;
} }
private T getResult() throws FailedConnectionException, ForbiddenException { private T getResult() throws MissingRepositoryException, FailedConnectionException, ForbiddenException {
try { try {
HttpResponse response = future.get(); HttpResponse response = future.get();
Log.info( Log.info(
@ -68,12 +69,30 @@ public abstract class Request<T extends Result> {
throw new FailedConnectionException(); throw new FailedConnectionException();
} catch (ExecutionException e) { } catch (ExecutionException e) {
Throwable cause = e.getCause(); Throwable cause = e.getCause();
if (cause instanceof HttpResponseException && if (cause instanceof HttpResponseException) {
(((HttpResponseException) cause).getStatusCode() == HttpResponseException httpCause = (HttpResponseException) cause;
HttpServletResponse.SC_UNAUTHORIZED || int sc = httpCause.getStatusCode();
((HttpResponseException) cause).getStatusCode() == if (sc == HttpServletResponse.SC_UNAUTHORIZED || sc == HttpServletResponse.SC_FORBIDDEN) {
HttpServletResponse.SC_FORBIDDEN)) { throw new ForbiddenException();
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 { } else {
throw new FailedConnectionException(cause); throw new FailedConnectionException(cause);
} }

View file

@ -42,8 +42,14 @@ public class WLGitBridgeIntegrationTest {
put("canCloneMultipleRepositories", new HashMap<String, SnapshotAPIState>() {{ put("canCloneMultipleRepositories", new HashMap<String, SnapshotAPIState>() {{
put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/canCloneMultipleRepositories/state/state.json")).build()); put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/canCloneMultipleRepositories/state/state.json")).build());
}}); }});
put("cannotCloneAProtectedProject", new HashMap<String, SnapshotAPIState>() {{ put("cannotCloneAProtectedProjectWithoutAuthentication", new HashMap<String, SnapshotAPIState>() {{
put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/cannotCloneAProtectedProject/state/state.json")).build()); 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("canPullAModifiedTexFile", new HashMap<String, SnapshotAPIState>() {{
put("base", new SnapshotAPIStateBuilder(getResourceAsStream("/canPullAModifiedTexFile/base/state.json")).build()); put("base", new SnapshotAPIStateBuilder(getResourceAsStream("/canPullAModifiedTexFile/base/state.json")).build());
@ -727,6 +733,60 @@ public class WLGitBridgeIntegrationTest {
wlgb.stop(); 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( private String makeConfigFile(
int port, int port,
int apiPort int apiPort

View file

@ -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"
}
}
]

View file

@ -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"
}
}
]