From 6d563ed40e565c9e4a503becca1ccca8338abaf8 Mon Sep 17 00:00:00 2001 From: Winston Li Date: Mon, 19 Dec 2016 12:56:58 +0000 Subject: [PATCH] Better javadoc, improve handling of submodules --- .../uk/ac/ic/wlgitbridge/bridge/Bridge.java | 2 +- .../bridge/repo/GitProjectRepo.java | 3 +- .../wlgitbridge/bridge/repo/ProjectRepo.java | 3 +- .../git/exception/InvalidGitRepository.java | 22 +++++++++ .../git/handler/hook/WriteLatexPutHook.java | 4 +- .../git/util/RepositoryObjectTreeWalker.java | 11 +++-- .../ic/wlgitbridge/server/Oauth2Filter.java | 29 +++++++++-- .../snapshot/base/SnapshotAPIRequest.java | 7 +++ .../WLGitBridgeIntegrationTest.java | 40 +++++++++++++++ .../state/state.json | 46 ++++++++++++++++++ .../state/testproj/foo/bar/test.tex | 1 + .../state/testproj/main.tex | 1 + .../min_mean_wait_evm_7_eps_150dpi.png | Bin 0 -> 10402 bytes 13 files changed, 156 insertions(+), 13 deletions(-) create mode 100644 services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/git/exception/InvalidGitRepository.java create mode 100644 services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/pushSubmoduleFailsWithInvalidGitRepo/state/state.json create mode 100644 services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/pushSubmoduleFailsWithInvalidGitRepo/state/testproj/foo/bar/test.tex create mode 100644 services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/pushSubmoduleFailsWithInvalidGitRepo/state/testproj/main.tex create mode 100644 services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/pushSubmoduleFailsWithInvalidGitRepo/state/testproj/min_mean_wait_evm_7_eps_150dpi.png diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/Bridge.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/Bridge.java index 2a5e550021..38ed53f504 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/Bridge.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/Bridge.java @@ -678,7 +678,7 @@ public class Bridge { private void makeCommitsFromSnapshots( ProjectRepo repo, Collection snapshots - ) throws IOException, SizeLimitExceededException { + ) throws IOException, GitUserException { String name = repo.getProjectName(); for (Snapshot snapshot : snapshots) { Map fileTable = repo.getFiles(); diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/repo/GitProjectRepo.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/repo/GitProjectRepo.java index 618af58f23..741030be1a 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/repo/GitProjectRepo.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/repo/GitProjectRepo.java @@ -9,6 +9,7 @@ import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.storage.file.FileRepositoryBuilder; import uk.ac.ic.wlgitbridge.data.filestore.GitDirectoryContents; import uk.ac.ic.wlgitbridge.data.filestore.RawFile; +import uk.ac.ic.wlgitbridge.git.exception.GitUserException; import uk.ac.ic.wlgitbridge.git.exception.SizeLimitExceededException; import uk.ac.ic.wlgitbridge.git.util.RepositoryObjectTreeWalker; import uk.ac.ic.wlgitbridge.util.Log; @@ -62,7 +63,7 @@ public class GitProjectRepo implements ProjectRepo { @Override public Map getFiles() - throws IOException, SizeLimitExceededException { + throws IOException, GitUserException { Preconditions.checkState(repository.isPresent()); return new RepositoryObjectTreeWalker( repository.get() diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/repo/ProjectRepo.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/repo/ProjectRepo.java index c327ac1315..c3ac9c2f3a 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/repo/ProjectRepo.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/bridge/repo/ProjectRepo.java @@ -2,6 +2,7 @@ package uk.ac.ic.wlgitbridge.bridge.repo; import uk.ac.ic.wlgitbridge.data.filestore.GitDirectoryContents; import uk.ac.ic.wlgitbridge.data.filestore.RawFile; +import uk.ac.ic.wlgitbridge.git.exception.GitUserException; import uk.ac.ic.wlgitbridge.git.exception.SizeLimitExceededException; import java.io.IOException; @@ -24,7 +25,7 @@ public interface ProjectRepo { ) throws IOException; Map getFiles( - ) throws IOException, SizeLimitExceededException; + ) throws IOException, GitUserException; Collection commitAndGetMissing( GitDirectoryContents gitDirectoryContents diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/git/exception/InvalidGitRepository.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/git/exception/InvalidGitRepository.java new file mode 100644 index 0000000000..465a3fc726 --- /dev/null +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/git/exception/InvalidGitRepository.java @@ -0,0 +1,22 @@ +package uk.ac.ic.wlgitbridge.git.exception; + +import java.util.Arrays; +import java.util.List; + +public class InvalidGitRepository extends GitUserException { + + @Override + public String getMessage() { + return "invalid git repo"; + } + + @Override + public List getDescriptionLines() { + return Arrays.asList( + "Your Git repository is invalid.", + "If your project contains a Git submodule,", + "please remove it and try again." + ); + } + +} diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/git/handler/hook/WriteLatexPutHook.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/git/handler/hook/WriteLatexPutHook.java index f35fd0fc54..50fe9ae56d 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/git/handler/hook/WriteLatexPutHook.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/git/handler/hook/WriteLatexPutHook.java @@ -74,7 +74,7 @@ public class WriteLatexPutHook implements PreReceiveHook { ); } catch (OutOfDateException e) { receiveCommand.setResult(Result.REJECTED_NONFASTFORWARD); - } catch (SnapshotPostException e) { + } catch (GitUserException e) { handleSnapshotPostException(receivePack, receiveCommand, e); } catch (Throwable t) { Log.warn("Throwable on pre receive: ", t); @@ -90,7 +90,7 @@ public class WriteLatexPutHook implements PreReceiveHook { private void handleSnapshotPostException( ReceivePack receivePack, ReceiveCommand receiveCommand, - SnapshotPostException e + GitUserException e ) { String message = e.getMessage(); receivePack.sendError(message); diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/git/util/RepositoryObjectTreeWalker.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/git/util/RepositoryObjectTreeWalker.java index b31a168106..262cdd5d17 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/git/util/RepositoryObjectTreeWalker.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/git/util/RepositoryObjectTreeWalker.java @@ -8,6 +8,7 @@ import org.eclipse.jgit.treewalk.TreeWalk; import uk.ac.ic.wlgitbridge.data.filestore.RawDirectory; import uk.ac.ic.wlgitbridge.data.filestore.RawFile; import uk.ac.ic.wlgitbridge.data.filestore.RepositoryFile; +import uk.ac.ic.wlgitbridge.git.exception.InvalidGitRepository; import uk.ac.ic.wlgitbridge.git.exception.SizeLimitExceededException; import java.io.IOException; @@ -44,7 +45,7 @@ public class RepositoryObjectTreeWalker { } public RawDirectory getDirectoryContents( - ) throws IOException, SizeLimitExceededException { + ) throws IOException, SizeLimitExceededException, InvalidGitRepository { return new RawDirectory(walkGitObjectTree()); } @@ -63,7 +64,7 @@ public class RepositoryObjectTreeWalker { } private Map walkGitObjectTree( - ) throws IOException, SizeLimitExceededException { + ) throws IOException, SizeLimitExceededException, InvalidGitRepository { Map fileContentsTable = new HashMap<>(); if (treeWalk == null) { return fileContentsTable; @@ -71,9 +72,13 @@ public class RepositoryObjectTreeWalker { while (treeWalk.next()) { String path = treeWalk.getPathString(); + ObjectId objectId = treeWalk.getObjectId(0); + if (!repository.hasObject(objectId)) { + throw new InvalidGitRepository(); + } try { byte[] content = repository.open( - treeWalk.getObjectId(0) + objectId ).getBytes(); fileContentsTable.put(path, new RepositoryFile(path, content)); } catch (LargeObjectException e) { diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/Oauth2Filter.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/Oauth2Filter.java index d2b650d717..32d8e52217 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/Oauth2Filter.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/server/Oauth2Filter.java @@ -8,6 +8,7 @@ import uk.ac.ic.wlgitbridge.application.config.Oauth2; import uk.ac.ic.wlgitbridge.snapshot.base.ForbiddenException; import uk.ac.ic.wlgitbridge.snapshot.getdoc.GetDocRequest; import uk.ac.ic.wlgitbridge.util.Instance; +import uk.ac.ic.wlgitbridge.util.Log; import uk.ac.ic.wlgitbridge.util.Util; import javax.servlet.*; @@ -34,6 +35,17 @@ public class Oauth2Filter implements Filter { @Override public void init(FilterConfig filterConfig) {} + /** + * The original request from git will not contain the Authorization header. + * + * So, for projects that need auth, we return 401. Git will swallow this + * and prompt the user for user/pass, and then make a brand new request. + * @param servletRequest + * @param servletResponse + * @param filterChain + * @throws IOException + * @throws ServletException + */ @Override public void doFilter( ServletRequest servletRequest, @@ -44,24 +56,29 @@ public class Oauth2Filter implements Filter { ((Request) servletRequest).getRequestURI().split("/")[1], ".git" ); + Log.info("[{}] Checking if auth needed", project); GetDocRequest doc = new GetDocRequest(project); doc.request(); try { doc.getResult(); } catch (ForbiddenException e) { + Log.info("[{}] Auth needed", project); getAndInjectCredentials( + project, servletRequest, servletResponse, filterChain ); return; } + Log.info("[{}] Auth not needed"); filterChain.doFilter(servletRequest, servletResponse); } // TODO: this is ridiculous. Check for error cases first, then return/throw // TODO: also, use an Optional credential, since we treat it as optional private void getAndInjectCredentials( + String projectName, ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain @@ -71,6 +88,7 @@ public class Oauth2Filter implements Filter { String authHeader = request.getHeader("Authorization"); if (authHeader != null) { + Log.info("[{}] Authorization header present"); StringTokenizer st = new StringTokenizer(authHeader); if (st.hasMoreTokens()) { String basic = st.nextToken(); @@ -100,10 +118,9 @@ public class Oauth2Filter implements Filter { oauth2.getOauth2ClientID(), oauth2.getOauth2ClientSecret() ) - ) - .execute().getAccessToken(); + ).execute().getAccessToken(); } catch (TokenResponseException e) { - unauthorized(response); + unauthorized(projectName, response); return; } final Credential cred = new Credential.Builder( @@ -118,7 +135,7 @@ public class Oauth2Filter implements Filter { servletResponse ); } else { - unauthorized(response); + unauthorized(projectName, response); } } catch (UnsupportedEncodingException e) { throw new Error("Couldn't retrieve authentication", e); @@ -126,7 +143,7 @@ public class Oauth2Filter implements Filter { } } } else { - unauthorized(response); + unauthorized(projectName, response); } } @@ -134,8 +151,10 @@ public class Oauth2Filter implements Filter { public void destroy() {} private void unauthorized( + String projectName, ServletResponse servletResponse ) throws IOException { + Log.info("[{}] Unauthorized", projectName); HttpServletResponse response = (HttpServletResponse) servletResponse; response.setContentType("text/plain"); response.setHeader("WWW-Authenticate", "Basic realm=\"Git Bridge\""); diff --git a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/snapshot/base/SnapshotAPIRequest.java b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/snapshot/base/SnapshotAPIRequest.java index b659653760..d9a77e1df4 100644 --- a/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/snapshot/base/SnapshotAPIRequest.java +++ b/services/git-bridge/src/main/java/uk/ac/ic/wlgitbridge/snapshot/base/SnapshotAPIRequest.java @@ -39,6 +39,13 @@ public abstract class SnapshotAPIRequest extends Request { ).intercept(request1); oauth2.intercept(request1); }); + } else { + request.setInterceptor(request1 -> { + new BasicAuthentication( + USERNAME, + PASSWORD + ).intercept(request1); + }); } } diff --git a/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/WLGitBridgeIntegrationTest.java b/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/WLGitBridgeIntegrationTest.java index 67d3e985ed..6d8a2b45ac 100644 --- a/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/WLGitBridgeIntegrationTest.java +++ b/services/git-bridge/src/test/java/uk/ac/ic/wlgitbridge/application/WLGitBridgeIntegrationTest.java @@ -107,6 +107,9 @@ public class WLGitBridgeIntegrationTest { put("wlgbCanSwapProjects", new HashMap() {{ put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/wlgbCanSwapProjects/state/state.json")).build()); }}); + put("pushSubmoduleFailsWithInvalidGitRepo", new HashMap() {{ + put("state", new SnapshotAPIStateBuilder(getResourceAsStream("/pushSubmoduleFailsWithInvalidGitRepo/state/state.json")).build()); + }}); }}; @Rule @@ -624,6 +627,43 @@ public class WLGitBridgeIntegrationTest { wlgb.stop(); } + private static final List EXPECTED_OUT_PUSH_SUBMODULE = Arrays.asList( + "remote: hint: Your Git repository is invalid.", + "remote: hint: If your project contains a Git submodule,", + "remote: hint: please remove it and try again.", + "To http://127.0.0.1:33875/testproj.git", + "! [remote rejected] master -> master (invalid git repo)", + "error: failed to push some refs to 'http://127.0.0.1:33875/testproj.git'" + ); + + @Test + public void pushSubmoduleFailsWithInvalidGitRepo() throws IOException, GitAPIException, InterruptedException { + MockSnapshotServer server = new MockSnapshotServer(3875, getResource("/pushSubmoduleFailsWithInvalidGitRepo").toFile()); + server.start(); + server.setState(states.get("pushSubmoduleFailsWithInvalidGitRepo").get("state")); + GitBridgeApp wlgb = new GitBridgeApp(new String[] { + makeConfigFile(33875, 3875) + }); + wlgb.run(); + File dir = folder.newFolder(); + File testprojDir = cloneRepository("testproj", 33875, dir); + runtime.exec("mkdir sub", null, testprojDir).waitFor(); + File sub = new File(testprojDir, "sub"); + runtime.exec("touch sub.txt", null, sub).waitFor(); + runtime.exec("git init", null, sub).waitFor(); + runtime.exec("git add -A", null, sub).waitFor(); + runtime.exec("git commit -m \"sub\"", null, sub).waitFor(); + runtime.exec("git add -A", null, testprojDir).waitFor(); + runtime.exec("git commit -m \"push\"", null, testprojDir).waitFor(); + Process gitPush = runtime.exec("git push", null, testprojDir); + int pushExitCode = gitPush.waitFor(); + wlgb.stop(); + assertEquals(1, pushExitCode); + List actual = Util.linesFromStream(gitPush.getErrorStream(), 2, "[K"); + assertEquals(EXPECTED_OUT_PUSH_SUBMODULE, actual); + wlgb.stop(); + } + private File cloneRepository(String repositoryName, int port, File dir) throws IOException, InterruptedException { String repo = "git clone http://127.0.0.1:" + port + "/" + repositoryName + ".git"; Process gitProcess = runtime.exec(repo, null, dir); diff --git a/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/pushSubmoduleFailsWithInvalidGitRepo/state/state.json b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/pushSubmoduleFailsWithInvalidGitRepo/state/state.json new file mode 100644 index 0000000000..d876a5492c --- /dev/null +++ b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/pushSubmoduleFailsWithInvalidGitRepo/state/state.json @@ -0,0 +1,46 @@ +[ + { + "project": "testproj", + "getDoc": { + "versionID": 1, + "createdAt": "2014-11-30T18:40:58.123Z", + "email": "jdleesmiller+1@gmail.com", + "name": "John+1" + }, + "getSavedVers": [ + { + "versionID": 1, + "comment": "added more info on doc GET and error details", + "email": "jdleesmiller+1@gmail.com", + "name": "John+1", + "createdAt": "2014-11-30T18:47:01.333Z" + } + ], + "getForVers": [ + { + "versionID": 1, + "srcs": [ + { + "content": "content\n", + "path": "main.tex" + }, + { + "content": "This text is from another file.", + "path": "foo/bar/test.tex" + } + ], + "atts": [ + { + "url": "http://127.0.0.1:3875/state/testproj/min_mean_wait_evm_7_eps_150dpi.png", + "path": "min_mean_wait_evm_7_eps_150dpi.png" + } + ] + } + ], + "push": "success", + "postback": { + "type": "success", + "versionID": 2 + } + } +] \ No newline at end of file diff --git a/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/pushSubmoduleFailsWithInvalidGitRepo/state/testproj/foo/bar/test.tex b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/pushSubmoduleFailsWithInvalidGitRepo/state/testproj/foo/bar/test.tex new file mode 100644 index 0000000000..046794f19a --- /dev/null +++ b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/pushSubmoduleFailsWithInvalidGitRepo/state/testproj/foo/bar/test.tex @@ -0,0 +1 @@ +This text is from another file. \ No newline at end of file diff --git a/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/pushSubmoduleFailsWithInvalidGitRepo/state/testproj/main.tex b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/pushSubmoduleFailsWithInvalidGitRepo/state/testproj/main.tex new file mode 100644 index 0000000000..d95f3ad14d --- /dev/null +++ b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/pushSubmoduleFailsWithInvalidGitRepo/state/testproj/main.tex @@ -0,0 +1 @@ +content diff --git a/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/pushSubmoduleFailsWithInvalidGitRepo/state/testproj/min_mean_wait_evm_7_eps_150dpi.png b/services/git-bridge/src/test/resources/uk/ac/ic/wlgitbridge/WLGitBridgeIntegrationTest/pushSubmoduleFailsWithInvalidGitRepo/state/testproj/min_mean_wait_evm_7_eps_150dpi.png new file mode 100644 index 0000000000000000000000000000000000000000..74e1fcd990e0bcf170aeddc797d74fbfab0c4e08 GIT binary patch literal 10402 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