| // Copyright (C) 2018 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.acceptance.git; |
| |
| import static com.google.common.truth.Truth.assertThat; |
| import static com.google.common.truth.Truth.assertWithMessage; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.allow; |
| import static com.google.gerrit.acceptance.testsuite.project.TestProjectUpdate.block; |
| import static com.google.gerrit.git.testing.PushResultSubject.assertThat; |
| import static com.google.gerrit.server.group.SystemGroupBackend.REGISTERED_USERS; |
| import static java.util.stream.Collectors.toList; |
| |
| import com.google.common.collect.ImmutableList; |
| import com.google.common.collect.ImmutableMap; |
| import com.google.gerrit.acceptance.AbstractDaemonTest; |
| import com.google.gerrit.acceptance.testsuite.project.ProjectOperations; |
| import com.google.gerrit.acceptance.testsuite.request.RequestScopeOperations; |
| import com.google.gerrit.common.data.GlobalCapability; |
| import com.google.gerrit.entities.AccessSection; |
| import com.google.gerrit.entities.Change; |
| import com.google.gerrit.entities.PatchSet; |
| import com.google.gerrit.entities.Permission; |
| import com.google.gerrit.extensions.api.changes.ReviewInput; |
| import com.google.gerrit.extensions.client.ProjectState; |
| import com.google.gerrit.extensions.common.ChangeInput; |
| import com.google.gerrit.server.project.ProjectConfig; |
| import com.google.inject.Inject; |
| import java.util.Arrays; |
| import java.util.function.Consumer; |
| import org.eclipse.jgit.api.PushCommand; |
| import org.eclipse.jgit.junit.TestRepository; |
| import org.eclipse.jgit.lib.BlobBasedConfig; |
| import org.eclipse.jgit.lib.Config; |
| import org.eclipse.jgit.lib.ObjectId; |
| import org.eclipse.jgit.lib.Repository; |
| import org.eclipse.jgit.transport.PushResult; |
| import org.eclipse.jgit.transport.RefSpec; |
| import org.eclipse.jgit.transport.RemoteRefUpdate; |
| import org.eclipse.jgit.transport.RemoteRefUpdate.Status; |
| import org.eclipse.jgit.transport.TrackingRefUpdate; |
| import org.junit.Before; |
| import org.junit.Test; |
| |
| public class PushPermissionsIT extends AbstractDaemonTest { |
| @Inject private ProjectOperations projectOperations; |
| @Inject private RequestScopeOperations requestScopeOperations; |
| |
| @Before |
| public void setUp() throws Exception { |
| try (ProjectConfigUpdate u = updateProject(allProjects)) { |
| ProjectConfig cfg = u.getConfig(); |
| |
| // Remove push-related permissions, so they can be added back individually by test methods. |
| removeAllBranchPermissions( |
| cfg, |
| Permission.ADD_PATCH_SET, |
| Permission.CREATE, |
| Permission.DELETE, |
| Permission.PUSH, |
| Permission.PUSH_MERGE, |
| Permission.SUBMIT); |
| removeAllGlobalCapabilities(cfg, GlobalCapability.ADMINISTRATE_SERVER); |
| u.save(); |
| } |
| |
| // Include some auxiliary permissions. |
| projectOperations |
| .allProjectsForUpdate() |
| .add(allow(Permission.FORGE_AUTHOR).ref("refs/*").group(REGISTERED_USERS)) |
| .add(allow(Permission.FORGE_COMMITTER).ref("refs/*").group(REGISTERED_USERS)) |
| .update(); |
| } |
| |
| @Test |
| public void mixingMagicAndRegularPush() throws Exception { |
| testRepo.branch("HEAD").commit().create(); |
| PushResult r = push("HEAD:refs/heads/master", "HEAD:refs/for/master"); |
| |
| String msg = "cannot combine normal pushes and magic pushes"; |
| assertThat(r.getRemoteUpdate("refs/heads/master").getStatus()).isNotEqualTo(Status.OK); |
| assertThat(r.getRemoteUpdate("refs/for/master").getStatus()).isNotEqualTo(Status.OK); |
| assertThat(r.getRemoteUpdate("refs/for/master").getMessage()).isEqualTo(msg); |
| } |
| |
| @Test |
| public void pushNewCommitsRequiresPushPermission() throws Exception { |
| testRepo.branch("HEAD").commit().create(); |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.CREATE).ref("refs/*").group(REGISTERED_USERS)) |
| .update(); |
| |
| PushResult r = push("HEAD:refs/heads/newbranch"); |
| |
| String msg = "update for creating new commit object not permitted"; |
| RemoteRefUpdate rru = r.getRemoteUpdate("refs/heads/newbranch"); |
| assertThat(rru.getStatus()).isNotEqualTo(Status.OK); |
| assertThat(rru.getMessage()).contains(msg); |
| |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.PUSH).ref("refs/*").group(REGISTERED_USERS)) |
| .update(); |
| RemoteRefUpdate success = |
| push("HEAD:refs/heads/newbranch").getRemoteUpdate("refs/heads/newbranch"); |
| assertThat(success.getStatus()).isEqualTo(Status.OK); |
| } |
| |
| @Test |
| public void fastForwardUpdateDenied() throws Exception { |
| testRepo.branch("HEAD").commit().create(); |
| PushResult r = push("HEAD:refs/heads/master"); |
| assertThat(r) |
| .onlyRef("refs/heads/master") |
| .isRejected("prohibited by Gerrit: not permitted: update"); |
| assertThat(r) |
| .hasMessages( |
| "error: branch refs/heads/master:", |
| "Push to refs/for/master to create a review, or get 'Push' rights to update the branch.", |
| "User: admin", |
| "Contact an administrator to fix the permissions"); |
| assertThat(r).hasProcessed(ImmutableMap.of("refs", 1)); |
| } |
| |
| @Test |
| public void nonFastForwardUpdateDenied() throws Exception { |
| ObjectId commit = testRepo.commit().create(); |
| PushResult r = push("+" + commit.name() + ":refs/heads/master"); |
| assertThat(r) |
| .onlyRef("refs/heads/master") |
| .isRejected("prohibited by Gerrit: not permitted: force update"); |
| assertThat(r).hasProcessed(ImmutableMap.of("refs", 1)); |
| } |
| |
| @Test |
| public void deleteDenied() throws Exception { |
| PushResult r = push(":refs/heads/master"); |
| assertThat(r) |
| .onlyRef("refs/heads/master") |
| .isRejected("prohibited by Gerrit: not permitted: delete"); |
| assertThat(r) |
| .hasMessages( |
| "error: branch refs/heads/master:", |
| "You need 'Delete Reference' rights or 'Push' rights with the ", |
| "'Force Push' flag set to delete references.", |
| "User: admin", |
| "Contact an administrator to fix the permissions"); |
| assertThat(r).hasProcessed(ImmutableMap.of("refs", 1)); |
| } |
| |
| @Test |
| public void createDenied() throws Exception { |
| testRepo.branch("HEAD").commit().create(); |
| PushResult r = push("HEAD:refs/heads/newbranch"); |
| assertThat(r) |
| .onlyRef("refs/heads/newbranch") |
| .isRejected("prohibited by Gerrit: not permitted: create"); |
| assertThat(r).containsMessages("You need 'Create' rights to create new references."); |
| assertThat(r).hasProcessed(ImmutableMap.of("refs", 1)); |
| } |
| |
| @Test |
| public void createDeniedIfUserCantRead() throws Exception { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(block(Permission.READ).ref("refs/*").group(REGISTERED_USERS)) |
| .add(allow(Permission.PUSH).ref("refs/*").group(REGISTERED_USERS)) |
| .update(); |
| testRepo.branch("HEAD").commit().create(); |
| PushResult r = push("HEAD:refs/for/master"); |
| assertThat(r) |
| .onlyRef("refs/for/master") |
| .isRejected("prohibited by Gerrit: not permitted: read on refs/heads/master"); |
| assertThat(r).hasProcessed(ImmutableMap.of("refs", 1)); |
| } |
| |
| @Test |
| public void groupRefsByMessage() throws Exception { |
| try (Repository repo = repoManager.openRepository(project); |
| TestRepository<Repository> tr = new TestRepository<>(repo)) { |
| tr.branch("foo").commit().create(); |
| tr.branch("bar").commit().create(); |
| } |
| |
| testRepo.branch("HEAD").commit().create(); |
| PushResult r = push(":refs/heads/foo", ":refs/heads/bar", "HEAD:refs/heads/master"); |
| assertThat(r).ref("refs/heads/foo").isRejected("prohibited by Gerrit: not permitted: delete"); |
| assertThat(r).ref("refs/heads/bar").isRejected("prohibited by Gerrit: not permitted: delete"); |
| assertThat(r) |
| .ref("refs/heads/master") |
| .isRejected("prohibited by Gerrit: not permitted: update"); |
| assertThat(r) |
| .hasMessages( |
| "error: branches refs/heads/foo, refs/heads/bar:", |
| "You need 'Delete Reference' rights or 'Push' rights with the ", |
| "'Force Push' flag set to delete references.", |
| "error: branch refs/heads/master:", |
| "Push to refs/for/master to create a review, or get 'Push' rights to update the branch.", |
| "User: admin", |
| "Contact an administrator to fix the permissions"); |
| } |
| |
| @Test |
| public void readOnlyProjectRejectedBeforeTestingPermissions() throws Exception { |
| try (Repository repo = repoManager.openRepository(project)) { |
| try (ProjectConfigUpdate u = updateProject(project)) { |
| u.getConfig().updateProject(p -> p.setState(ProjectState.READ_ONLY)); |
| u.save(); |
| } |
| } |
| |
| PushResult r = push(":refs/heads/master"); |
| assertThat(r) |
| .onlyRef("refs/heads/master") |
| .isRejected("prohibited by Gerrit: project state does not permit write"); |
| assertThat(r).hasNoMessages(); |
| assertThat(r).hasProcessed(ImmutableMap.of("refs", 1)); |
| } |
| |
| @Test |
| public void refsMetaConfigUpdateRequiresProjectOwner() throws Exception { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.PUSH).ref("refs/meta/config").group(REGISTERED_USERS)) |
| .update(); |
| |
| forceFetch("refs/meta/config"); |
| ObjectId commit = testRepo.branch("refs/meta/config").commit().create(); |
| PushResult r = push(commit.name() + ":refs/meta/config"); |
| assertThat(r) |
| .onlyRef("refs/meta/config") |
| // ReceiveCommits theoretically has a different message when a WRITE_CONFIG check fails, but |
| // it never gets there, since DefaultPermissionBackend special-cases refs/meta/config and |
| // denies UPDATE if the user is not a project owner. |
| .isRejected("prohibited by Gerrit: not permitted: update"); |
| assertThat(r) |
| .hasMessages( |
| "error: branch refs/meta/config:", |
| "Configuration changes can only be pushed by project owners", |
| "who also have 'Push' rights on refs/meta/config", |
| "User: admin", |
| "Contact an administrator to fix the permissions"); |
| assertThat(r).hasProcessed(ImmutableMap.of("refs", 1)); |
| |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS)) |
| .update(); |
| |
| // Re-fetch refs/meta/config from the server because the grant changed it, and we want a |
| // fast-forward. |
| forceFetch("refs/meta/config"); |
| commit = testRepo.branch("refs/meta/config").commit().create(); |
| |
| assertThat(push(commit.name() + ":refs/meta/config")).onlyRef("refs/meta/config").isOk(); |
| } |
| |
| @Test |
| public void createChangeDenied() throws Exception { |
| testRepo.branch("HEAD").commit().create(); |
| PushResult r = push("HEAD:refs/for/master"); |
| assertThat(r) |
| .onlyRef("refs/for/master") |
| .isRejected("prohibited by Gerrit: not permitted: create change on refs/heads/master"); |
| assertThat(r) |
| .containsMessages( |
| "error: branch refs/for/master:", |
| "You need 'Create Change' rights to upload code review requests.", |
| "Verify that you are pushing to the right branch."); |
| assertThat(r).hasProcessed(ImmutableMap.of("refs", 1)); |
| } |
| |
| @Test |
| public void updateBySubmitDenied() throws Exception { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.PUSH).ref("refs/for/refs/heads/*").group(REGISTERED_USERS)) |
| .update(); |
| |
| ObjectId commit = |
| testRepo.branch("HEAD").commit().message("test commit").insertChangeId().create(); |
| assertThat(push("HEAD:refs/for/master")).onlyRef("refs/for/master").isOk(); |
| gApi.changes().id(commit.name()).current().review(ReviewInput.approve()); |
| |
| PushResult r = push("HEAD:refs/for/master%submit"); |
| assertThat(r) |
| .onlyRef("refs/for/master%submit") |
| .isRejected("prohibited by Gerrit: not permitted: update by submit on refs/heads/master"); |
| assertThat(r) |
| .containsMessages( |
| "You need 'Submit' rights on refs/for/ to submit changes during change upload."); |
| assertThat(r).hasProcessed(ImmutableMap.of("refs", 1)); |
| } |
| |
| @Test |
| public void addPatchSetDenied() throws Exception { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.PUSH).ref("refs/for/refs/heads/*").group(REGISTERED_USERS)) |
| .update(); |
| requestScopeOperations.setApiUser(user.id()); |
| ChangeInput ci = new ChangeInput(); |
| ci.project = project.get(); |
| ci.branch = "master"; |
| ci.subject = "A change"; |
| Change.Id id = Change.id(gApi.changes().create(ci).get()._number); |
| |
| requestScopeOperations.setApiUser(admin.id()); |
| ObjectId ps1Id = forceFetch(PatchSet.id(id, 1).toRefName()); |
| ObjectId ps2Id = testRepo.amend(ps1Id).add("file", "content").create(); |
| PushResult r = push(ps2Id.name() + ":refs/for/master"); |
| // Admin had ADD_PATCH_SET removed in setup. |
| assertThat(r) |
| .onlyRef("refs/for/master") |
| .isRejected("cannot add patch set to " + id.get() + "."); |
| assertThat(r).hasNoMessages(); |
| assertThat(r).hasProcessed(ImmutableMap.of("refs", 1)); |
| } |
| |
| @Test |
| public void skipValidationDenied() throws Exception { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.PUSH).ref("refs/heads/*").group(REGISTERED_USERS)) |
| .update(); |
| |
| testRepo.branch("HEAD").commit().create(); |
| PushResult r = |
| push(c -> c.setPushOptions(ImmutableList.of("skip-validation")), "HEAD:refs/heads/master"); |
| assertThat(r) |
| .onlyRef("refs/heads/master") |
| .isRejected("prohibited by Gerrit: not permitted: skip validation"); |
| assertThat(r) |
| .containsMessages( |
| "You need 'Forge Author', 'Forge Server', 'Forge Committer'", |
| "and 'Push Merge' rights to skip validation."); |
| assertThat(r).hasProcessed(ImmutableMap.of("refs", 1)); |
| } |
| |
| @Test |
| public void accessDatabaseForNoteDbDenied() throws Exception { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.PUSH).ref("refs/heads/*").group(REGISTERED_USERS)) |
| .update(); |
| |
| testRepo.branch("HEAD").commit().create(); |
| PushResult r = |
| push( |
| c -> c.setPushOptions(ImmutableList.of("notedb=allow")), |
| "HEAD:refs/changes/34/1234/meta"); |
| // Same rejection message regardless of whether NoteDb is actually enabled. |
| assertThat(r) |
| .onlyRef("refs/changes/34/1234/meta") |
| .isRejected("NoteDb update requires access database permission"); |
| assertThat(r).hasNoMessages(); |
| assertThat(r).hasProcessed(ImmutableMap.of("refs", 1)); |
| } |
| |
| @Test |
| public void administrateServerForUpdateParentDenied() throws Exception { |
| projectOperations |
| .project(project) |
| .forUpdate() |
| .add(allow(Permission.PUSH).ref("refs/meta/config").group(REGISTERED_USERS)) |
| .add(allow(Permission.OWNER).ref("refs/*").group(REGISTERED_USERS)) |
| .update(); |
| |
| String project2 = name("project2"); |
| projectOperations.newProject().name(project2).create(); |
| |
| ObjectId oldId = forceFetch("refs/meta/config"); |
| |
| Config cfg = new BlobBasedConfig(null, testRepo.getRepository(), oldId, "project.config"); |
| cfg.setString("access", null, "inheritFrom", project2); |
| ObjectId newId = |
| testRepo.branch("refs/meta/config").commit().add("project.config", cfg.toText()).create(); |
| |
| PushResult r = push(newId.name() + ":refs/meta/config"); |
| assertThat(r) |
| .onlyRef("refs/meta/config") |
| .isRejected("invalid project configuration: only Gerrit admin can set parent"); |
| assertThat(r).hasNoMessages(); |
| assertThat(r).hasProcessed(ImmutableMap.of("refs", 1)); |
| } |
| |
| private static void removeAllBranchPermissions(ProjectConfig cfg, String... permissions) { |
| for (AccessSection s : cfg.getAccessSections()) { |
| if (s.getName().startsWith("refs/heads/") |
| || s.getName().startsWith("refs/for/") |
| || s.getName().equals("refs/*")) { |
| cfg.upsertAccessSection( |
| s.getName(), |
| updatedSection -> { |
| Arrays.stream(permissions).forEach(p -> updatedSection.remove(Permission.builder(p))); |
| }); |
| } |
| } |
| } |
| |
| private static void removeAllGlobalCapabilities(ProjectConfig cfg, String... capabilities) { |
| Arrays.stream(capabilities) |
| .forEach( |
| c -> |
| cfg.upsertAccessSection( |
| AccessSection.GLOBAL_CAPABILITIES, |
| as -> { |
| as.upsertPermission(c).clearRules(); |
| })); |
| } |
| |
| private PushResult push(String... refSpecs) throws Exception { |
| return push(c -> {}, refSpecs); |
| } |
| |
| private PushResult push(Consumer<PushCommand> setUp, String... refSpecs) throws Exception { |
| PushCommand cmd = |
| testRepo |
| .git() |
| .push() |
| .setRemote("origin") |
| .setRefSpecs(Arrays.stream(refSpecs).map(RefSpec::new).collect(toList())); |
| setUp.accept(cmd); |
| Iterable<PushResult> results = cmd.call(); |
| assertWithMessage("expected 1 PushResult").that(results).hasSize(1); |
| return results.iterator().next(); |
| } |
| |
| private ObjectId forceFetch(String ref) throws Exception { |
| TrackingRefUpdate u = |
| testRepo.git().fetch().setRefSpecs("+" + ref + ":" + ref).call().getTrackingRefUpdate(ref); |
| assertThat(u).isNotNull(); |
| switch (u.getResult()) { |
| case NEW: |
| case FAST_FORWARD: |
| case FORCED: |
| break; |
| case IO_FAILURE: |
| case LOCK_FAILURE: |
| case NOT_ATTEMPTED: |
| case NO_CHANGE: |
| case REJECTED: |
| case REJECTED_CURRENT_BRANCH: |
| case REJECTED_MISSING_OBJECT: |
| case REJECTED_OTHER_REASON: |
| case RENAMED: |
| default: |
| assertWithMessage("fetch failed to update local %s: %s", ref, u.getResult()).fail(); |
| break; |
| } |
| return u.getNewObjectId(); |
| } |
| } |