| // Copyright (C) 2019 The Android Open Source Project |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package com.google.gerrit.integration.git; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.deny; |
| import static java.nio.charset.StandardCharsets.UTF_8; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.gerrit.acceptance.AccountCreator; |
| import com.google.gerrit.acceptance.GerritServer.TestSshServerAddress; |
| import com.google.gerrit.acceptance.GitClientVersion; |
| import com.google.gerrit.acceptance.StandaloneSiteTest; |
| import com.google.gerrit.acceptance.TestAccount; |
| import com.google.gerrit.acceptance.UseSsh; |
| import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.PatchSet; |
| import com.google.gerrit.entities.Permission; |
| import com.google.gerrit.entities.Project; |
| import com.google.gerrit.entities.RefNames; |
| import com.google.gerrit.extensions.api.GerritApi; |
| import com.google.gerrit.extensions.common.ChangeInput; |
| import com.google.gerrit.server.config.AllProjectsName; |
| import com.google.gerrit.server.config.GerritServerConfig; |
| import com.google.gerrit.server.group.SystemGroupBackend; |
| import com.google.inject.Inject; |
| import java.io.File; |
| import java.net.InetSocketAddress; |
| import org.eclipse.jgit.lib.Config; |
| import org.junit.BeforeClass; |
| import org.junit.Test; |
| |
| @UseSsh |
| public class GitProtocolV2IT extends StandaloneSiteTest { |
| private static final String ADMIN_PASSWORD = "secret"; |
| private final String[] SSH_KEYGEN_CMD = |
| new String[] {"ssh-keygen", "-t", "rsa", "-q", "-P", "", "-f"}; |
| private final String[] GIT_LS_REMOTE = |
| new String[] {"git", "-c", "protocol.version=2", "ls-remote", "-o", "trace=12345"}; |
| private final String[] GIT_CLONE_MIRROR = |
| new String[] {"git", "-c", "protocol.version=2", "clone", "--mirror"}; |
| private final String[] GIT_FETCH = new String[] {"git", "-c", "protocol.version=2", "fetch"}; |
| private final String[] GIT_INIT = new String[] {"git", "init"}; |
| private final String GIT_SSH_COMMAND = |
| "ssh -o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i"; |
| |
| @Inject private GerritApi gApi; |
| @Inject private AccountCreator accountCreator; |
| @Inject private ProjectOperations projectOperations; |
| @Inject private @TestSshServerAddress InetSocketAddress sshAddress; |
| @Inject private @GerritServerConfig Config config; |
| @Inject private AllProjectsName allProjectsName; |
| |
| @BeforeClass |
| public static void assertGitClientVersion() throws Exception { |
| // Minimum required git-core version that supports wire protocol v2 is 2.18.0 |
| GitClientVersion requiredGitVersion = new GitClientVersion(2, 18, 0); |
| GitClientVersion actualGitVersion = |
| new GitClientVersion(execute(ImmutableList.of("git", "version"), new File("/"))); |
| // If git client version cannot be updated, consider to skip this tests. Due to |
| // an existing issue in bazel, JUnit assumption violation feature cannot be used. |
| assertThat(actualGitVersion).isAtLeast(requiredGitVersion); |
| } |
| |
| @Test |
| public void testGitWireProtocolV2WithSsh() throws Exception { |
| try (ServerContext ctx = startServer()) { |
| ctx.getInjector().injectMembers(this); |
| |
| // Create project |
| Project.NameKey project = Project.nameKey("foo"); |
| gApi.projects().create(project.get()); |
| |
| // Set up project permission |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(deny(Permission.READ).ref("refs/heads/*").group(SystemGroupBackend.ANONYMOUS_USERS)) |
| .add( |
| allow(Permission.READ) |
| .ref("refs/heads/master") |
| .group(SystemGroupBackend.REGISTERED_USERS)) |
| .update(); |
| |
| // Retrieve HTTP url |
| String url = config.getString("gerrit", null, "canonicalweburl"); |
| String urlDestinationTemplate = |
| url.substring(0, 7) |
| + "%s:secret@" |
| + url.substring(7, url.length()) |
| + "/a/" |
| + project.get(); |
| |
| // Retrieve SSH host and port |
| String sshDestinationTemplate = |
| "ssh://%s@" + sshAddress.getHostName() + ":" + sshAddress.getPort() + "/" + project.get(); |
| |
| // Admin user was already created by the base class |
| setUpUserAuthentication(admin.username()); |
| |
| // Create non-admin user |
| TestAccount user = accountCreator.user1(); |
| setUpUserAuthentication(user.username()); |
| |
| // Prepare data for new change on master branch |
| ChangeInput in = new ChangeInput(project.get(), "master", "Test public change"); |
| in.newBranch = true; |
| |
| // Create new change and retrieve SHA1 for the created patch set |
| String commit = |
| gApi.changes() |
| .id(gApi.changes().create(in).info().changeId) |
| .current() |
| .commit(false) |
| .commit; |
| |
| // Prepare new change on secret branch |
| in = new ChangeInput(project.get(), ADMIN_PASSWORD, "Test secret change"); |
| in.newBranch = true; |
| |
| // Create new change and retrieve SHA1 for the created patch set |
| String secretCommit = |
| gApi.changes() |
| .id(gApi.changes().create(in).info().changeId) |
| .current() |
| .commit(false) |
| .commit; |
| |
| // Read refs from target repository using git wire protocol v2 over HTTP for admin user |
| String out = |
| execute( |
| ImmutableList.<String>builder() |
| .add(GIT_LS_REMOTE) |
| .add(String.format(urlDestinationTemplate, admin.username())) |
| .build(), |
| ImmutableMap.of("GIT_TRACE_PACKET", "1")); |
| |
| assertGitProtocolV2Refs(commit, out); |
| assertThat(out).contains(secretCommit); |
| |
| // Read refs from target repository using git wire protocol v2 over SSH for admin user |
| out = |
| execute( |
| ImmutableList.<String>builder() |
| .add(GIT_LS_REMOTE) |
| .add(String.format(sshDestinationTemplate, admin.username())) |
| .build(), |
| ImmutableMap.of( |
| "GIT_SSH_COMMAND", |
| GIT_SSH_COMMAND |
| + sitePaths.data_dir.resolve(String.format("id_rsa_%s", admin.username())), |
| "GIT_TRACE_PACKET", |
| "1")); |
| |
| assertGitProtocolV2Refs(commit, out); |
| assertThat(out).contains(secretCommit); |
| |
| // Read refs from target repository using git wire protocol v2 over HTTP for non-admin user |
| out = |
| execute( |
| ImmutableList.<String>builder() |
| .add(GIT_LS_REMOTE) |
| .add(String.format(urlDestinationTemplate, user.username())) |
| .build(), |
| ImmutableMap.of("GIT_TRACE_PACKET", "1")); |
| |
| assertGitProtocolV2Refs(commit, out); |
| assertThat(out).doesNotContain(secretCommit); |
| |
| // Read refs from target repository using git wire protocol v2 over SSH for non-admin user |
| out = |
| execute( |
| ImmutableList.<String>builder() |
| .add(GIT_LS_REMOTE) |
| .add(String.format(sshDestinationTemplate, user.username())) |
| .build(), |
| ImmutableMap.of( |
| "GIT_SSH_COMMAND", |
| GIT_SSH_COMMAND |
| + sitePaths.data_dir.resolve(String.format("id_rsa_%s", user.username())), |
| "GIT_TRACE_PACKET", |
| "1")); |
| |
| assertGitProtocolV2Refs(commit, out); |
| assertThat(out).doesNotContain(secretCommit); |
| } |
| } |
| |
| @Test |
| public void testGitWireProtocolV2HidesRefMetaConfig() throws Exception { |
| try (ServerContext ctx = startServer()) { |
| ctx.getInjector().injectMembers(this); |
| String url = config.getString("gerrit", null, "canonicalweburl"); |
| |
| // Create project |
| Project.NameKey allRefsVisibleProject = Project.nameKey("all-refs-visible"); |
| gApi.projects().create(allRefsVisibleProject.get()); |
| |
| // Set up project permission to allow reading all refs |
| projectOperations |
| .project(allRefsVisibleProject) |
| .forUpdate() |
| .add(allow(Permission.READ).ref("refs/heads/*").group(SystemGroupBackend.ANONYMOUS_USERS)) |
| .add( |
| allow(Permission.READ) |
| .ref("refs/changes/*") |
| .group(SystemGroupBackend.ANONYMOUS_USERS)) |
| .update(); |
| |
| // Create new change and retrieve refs for the created patch set |
| ChangeInput visibleChangeIn = |
| new ChangeInput(allRefsVisibleProject.get(), "master", "Test public change"); |
| visibleChangeIn.newBranch = true; |
| int visibleChangeNumber = gApi.changes().create(visibleChangeIn).info()._number; |
| Change.Id changeId = Change.id(visibleChangeNumber); |
| String visibleChangeNumberRef = RefNames.patchSetRef(PatchSet.id(changeId, 1)); |
| String visibleChangeNumberMetaRef = RefNames.changeMetaRef(changeId); |
| |
| // Read refs from target repository using git wire protocol v2 over HTTP anonymously |
| String outAnonymousLsRemote = |
| execute( |
| ImmutableList.<String>builder() |
| .add(GIT_CLONE_MIRROR) |
| .add(url + "/" + allRefsVisibleProject.get()) |
| .build(), |
| ImmutableMap.of("GIT_TRACE_PACKET", "1")); |
| |
| assertThat(outAnonymousLsRemote).contains("git< version 2"); |
| assertThat(outAnonymousLsRemote).doesNotContain(RefNames.REFS_CONFIG); |
| assertThat(outAnonymousLsRemote).contains(visibleChangeNumberRef); |
| assertThat(outAnonymousLsRemote).contains(visibleChangeNumberMetaRef); |
| } |
| } |
| |
| @Test |
| public void testGitWireProtocolV2FetchIndividualRef() throws Exception { |
| try (ServerContext ctx = startServer()) { |
| ctx.getInjector().injectMembers(this); |
| |
| // Setup admin password |
| gApi.accounts().id(admin.username()).setHttpPassword(ADMIN_PASSWORD); |
| |
| // Get authenticated Git/HTTP URL |
| String urlWithCredentials = |
| config |
| .getString("gerrit", null, "canonicalweburl") |
| .replace("http://", "http://" + admin.username() + ":" + ADMIN_PASSWORD + "@"); |
| |
| // Create project |
| Project.NameKey privateProject = Project.nameKey("private-project"); |
| gApi.projects().create(privateProject.get()); |
| |
| // Disallow general read permissions for anonymous users |
| projectOperations |
| .project(allProjectsName) |
| .forUpdate() |
| .add(deny(Permission.READ).ref("refs/*").group(SystemGroupBackend.ANONYMOUS_USERS)) |
| .add( |
| allow(Permission.READ) |
| .ref("refs/heads/master") |
| .group(SystemGroupBackend.REGISTERED_USERS)) |
| .update(); |
| |
| // Set up project permission to allow registered users fetching changes/* |
| projectOperations |
| .project(privateProject) |
| .forUpdate() |
| .add( |
| allow(Permission.READ) |
| .ref("refs/changes/*") |
| .group(SystemGroupBackend.REGISTERED_USERS)) |
| .update(); |
| |
| // Create new change and retrieve refs for the created patch set |
| ChangeInput visibleChangeIn = |
| new ChangeInput(privateProject.get(), "master", "Test private change"); |
| visibleChangeIn.newBranch = true; |
| int visibleChangeNumber = gApi.changes().create(visibleChangeIn).info()._number; |
| Change.Id changeId = Change.id(visibleChangeNumber); |
| String visibleChangeNumberRef = RefNames.patchSetRef(PatchSet.id(changeId, 1)); |
| |
| // Fetch a single ref using git wire protocol v2 over HTTP with authentication |
| execute(GIT_INIT); |
| |
| String outFetchRef = |
| execute( |
| ImmutableList.<String>builder() |
| .add(GIT_FETCH) |
| .add(urlWithCredentials + "/" + privateProject.get()) |
| .add(visibleChangeNumberRef) |
| .build(), |
| ImmutableMap.of("GIT_TRACE_PACKET", "1")); |
| |
| assertThat(outFetchRef).contains("git< version 2"); |
| assertThat(outFetchRef).contains(visibleChangeNumberRef); |
| } |
| } |
| |
| private void setUpUserAuthentication(String username) throws Exception { |
| // Assign HTTP password to user |
| gApi.accounts().id(username).setHttpPassword(ADMIN_PASSWORD); |
| |
| // Generate private/public key for user |
| execute( |
| ImmutableList.<String>builder() |
| .add(SSH_KEYGEN_CMD) |
| .add(String.format("id_rsa_%s", username)) |
| .build()); |
| |
| // Read the content of generated public key and add it for the user in Gerrit |
| gApi.accounts() |
| .id(username) |
| .addSshKey( |
| new String( |
| java.nio.file.Files.readAllBytes( |
| sitePaths.data_dir.resolve(String.format("id_rsa_%s.pub", username))), |
| UTF_8)); |
| } |
| |
| private static void assertGitProtocolV2Refs(String commit, String out) { |
| assertThat(out).containsMatch("(git|ls-remote)< version 2"); |
| assertThat(out).contains("refs/changes/01/1/1"); |
| assertThat(out).contains("refs/changes/01/1/meta"); |
| assertThat(out).contains(commit); |
| } |
| |
| private String execute(String... cmds) throws Exception { |
| return execute(ImmutableList.<String>builder().add(cmds).build()); |
| } |
| |
| private String execute(ImmutableList<String> cmd) throws Exception { |
| return execute(cmd, sitePaths.data_dir.toFile(), ImmutableMap.of()); |
| } |
| |
| private String execute(ImmutableList<String> cmd, ImmutableMap<String, String> env) |
| throws Exception { |
| return execute(cmd, sitePaths.data_dir.toFile(), env); |
| } |
| |
| private static String execute(ImmutableList<String> cmd, File dir) throws Exception { |
| return execute(cmd, dir, ImmutableMap.of()); |
| } |
| } |