summaryrefslogtreecommitdiffstats
path: root/java/com/google/gerrit/server/permissions/RefControl.java
diff options
context:
space:
mode:
Diffstat (limited to 'java/com/google/gerrit/server/permissions/RefControl.java')
-rw-r--r--java/com/google/gerrit/server/permissions/RefControl.java638
1 files changed, 638 insertions, 0 deletions
diff --git a/java/com/google/gerrit/server/permissions/RefControl.java b/java/com/google/gerrit/server/permissions/RefControl.java
new file mode 100644
index 0000000000..74b04a32bf
--- /dev/null
+++ b/java/com/google/gerrit/server/permissions/RefControl.java
@@ -0,0 +1,638 @@
+// Copyright (C) 2010 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.server.permissions;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.flogger.FluentLogger;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.extensions.conditions.BooleanCondition;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RefNames;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.logging.CallerFinder;
+import com.google.gerrit.server.notedb.ChangeNotes;
+import com.google.gerrit.server.permissions.PermissionBackend.ForChange;
+import com.google.gerrit.server.permissions.PermissionBackend.ForRef;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.util.MagicBranch;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.util.Providers;
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Set;
+
+/** Manages access control for Git references (aka branches, tags). */
+class RefControl {
+ private static final FluentLogger logger = FluentLogger.forEnclosingClass();
+
+ private final ProjectControl projectControl;
+ private final String refName;
+
+ /** All permissions that apply to this reference. */
+ private final PermissionCollection relevant;
+
+ private final CallerFinder callerFinder;
+
+ // The next 4 members are cached canPerform() permissions.
+
+ private Boolean owner;
+ private Boolean canForgeAuthor;
+ private Boolean canForgeCommitter;
+ private Boolean isVisible;
+
+ RefControl(ProjectControl projectControl, String ref, PermissionCollection relevant) {
+ this.projectControl = projectControl;
+ this.refName = ref;
+ this.relevant = relevant;
+ this.callerFinder =
+ CallerFinder.builder()
+ .addTarget(PermissionBackend.class)
+ .matchSubClasses(true)
+ .matchInnerClasses(true)
+ .skip(1)
+ .build();
+ }
+
+ ProjectControl getProjectControl() {
+ return projectControl;
+ }
+
+ CurrentUser getUser() {
+ return projectControl.getUser();
+ }
+
+ /** Is this user a ref owner? */
+ boolean isOwner() {
+ if (owner == null) {
+ if (canPerform(Permission.OWNER)) {
+ owner = true;
+
+ } else {
+ owner = projectControl.isOwner();
+ }
+ }
+ return owner;
+ }
+
+ /** Can this user see this reference exists? */
+ boolean isVisible() {
+ if (isVisible == null) {
+ isVisible = getUser().isInternalUser() || canPerform(Permission.READ);
+ }
+ return isVisible;
+ }
+
+ /** @return true if this user can add a new patch set to this ref */
+ boolean canAddPatchSet() {
+ return projectControl
+ .controlForRef(MagicBranch.NEW_CHANGE + refName)
+ .canPerform(Permission.ADD_PATCH_SET);
+ }
+
+ /** @return true if this user can rebase changes on this ref */
+ boolean canRebase() {
+ return canPerform(Permission.REBASE);
+ }
+
+ /** @return true if this user can submit patch sets to this ref */
+ boolean canSubmit(boolean isChangeOwner) {
+ if (RefNames.REFS_CONFIG.equals(refName)) {
+ // Always allow project owners to submit configuration changes.
+ // Submitting configuration changes modifies the access control
+ // rules. Allowing this to be done by a non-project-owner opens
+ // a security hole enabling editing of access rules, and thus
+ // granting of powers beyond submitting to the configuration.
+ return projectControl.isOwner();
+ }
+ return canPerform(Permission.SUBMIT, isChangeOwner, false);
+ }
+
+ /** @return true if this user can force edit topic names. */
+ boolean canForceEditTopicName() {
+ return canPerform(Permission.EDIT_TOPIC_NAME, false, true);
+ }
+
+ /** @return true if this user can delete changes. */
+ boolean canDeleteChanges(boolean isChangeOwner) {
+ return canPerform(Permission.DELETE_CHANGES)
+ || (isChangeOwner && canPerform(Permission.DELETE_OWN_CHANGES, isChangeOwner, false));
+ }
+
+ /** The range of permitted values associated with a label permission. */
+ PermissionRange getRange(String permission) {
+ return getRange(permission, false);
+ }
+
+ /** The range of permitted values associated with a label permission. */
+ PermissionRange getRange(String permission, boolean isChangeOwner) {
+ if (Permission.hasRange(permission)) {
+ return toRange(permission, isChangeOwner);
+ }
+ return null;
+ }
+
+ /** True if the user has this permission. Works only for non labels. */
+ boolean canPerform(String permissionName) {
+ return canPerform(permissionName, false, false);
+ }
+
+ ForRef asForRef() {
+ return new ForRefImpl();
+ }
+
+ private boolean canUpload() {
+ return projectControl.controlForRef("refs/for/" + refName).canPerform(Permission.PUSH);
+ }
+
+ /** @return true if this user can submit merge patch sets to this ref */
+ private boolean canUploadMerges() {
+ return projectControl.controlForRef("refs/for/" + refName).canPerform(Permission.PUSH_MERGE);
+ }
+
+ /** @return true if the user can update the reference as a fast-forward. */
+ private boolean canUpdate() {
+ if (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner()) {
+ // Pushing requires being at least project owner, in addition to push.
+ // Pushing configuration changes modifies the access control
+ // rules. Allowing this to be done by a non-project-owner opens
+ // a security hole enabling editing of access rules, and thus
+ // granting of powers beyond pushing to the configuration.
+
+ // On the AllProjects project the owner access right cannot be assigned,
+ // this why for the AllProjects project we allow administrators to push
+ // configuration changes if they have push without being project owner.
+ if (!(projectControl.getProjectState().isAllProjects() && projectControl.isAdmin())) {
+ return false;
+ }
+ }
+ return canPerform(Permission.PUSH);
+ }
+
+ /** @return true if the user can rewind (force push) the reference. */
+ private boolean canForceUpdate() {
+ if (canPushWithForce()) {
+ return true;
+ }
+
+ switch (getUser().getAccessPath()) {
+ case GIT:
+ return false;
+
+ case JSON_RPC:
+ case REST_API:
+ case SSH_COMMAND:
+ case UNKNOWN:
+ case WEB_BROWSER:
+ default:
+ return (isOwner() && !isBlocked(Permission.PUSH, false, true)) || projectControl.isAdmin();
+ }
+ }
+
+ private boolean canPushWithForce() {
+ if (RefNames.REFS_CONFIG.equals(refName) && !projectControl.isOwner()) {
+ // Pushing requires being at least project owner, in addition to push.
+ // Pushing configuration changes modifies the access control
+ // rules. Allowing this to be done by a non-project-owner opens
+ // a security hole enabling editing of access rules, and thus
+ // granting of powers beyond pushing to the configuration.
+ return false;
+ }
+ return canPerform(Permission.PUSH, false, true);
+ }
+
+ /**
+ * Determines whether the user can delete the Git ref controlled by this object.
+ *
+ * @return {@code true} if the user specified can delete a Git ref.
+ */
+ private boolean canDelete() {
+ switch (getUser().getAccessPath()) {
+ case GIT:
+ return canPushWithForce() || canPerform(Permission.DELETE);
+
+ case JSON_RPC:
+ case REST_API:
+ case SSH_COMMAND:
+ case UNKNOWN:
+ case WEB_BROWSER:
+ default:
+ return
+ // We allow owner to delete refs even if they have no force-push rights. We forbid
+ // it if force push is blocked, though. See commit 40bd5741026863c99bea13eb5384bd27855c5e1b
+ (isOwner() && !isBlocked(Permission.PUSH, false, true))
+ || canPushWithForce()
+ || canPerform(Permission.DELETE)
+ || projectControl.isAdmin();
+ }
+ }
+
+ /** @return true if this user can forge the author line in a commit. */
+ private boolean canForgeAuthor() {
+ if (canForgeAuthor == null) {
+ canForgeAuthor = canPerform(Permission.FORGE_AUTHOR);
+ }
+ return canForgeAuthor;
+ }
+
+ /** @return true if this user can forge the committer line in a commit. */
+ private boolean canForgeCommitter() {
+ if (canForgeCommitter == null) {
+ canForgeCommitter = canPerform(Permission.FORGE_COMMITTER);
+ }
+ return canForgeCommitter;
+ }
+
+ /** @return true if this user can forge the server on the committer line. */
+ private boolean canForgeGerritServerIdentity() {
+ return canPerform(Permission.FORGE_SERVER);
+ }
+
+ private static boolean isAllow(PermissionRule pr, boolean withForce) {
+ return pr.getAction() == Action.ALLOW && (pr.getForce() || !withForce);
+ }
+
+ private static boolean isBlock(PermissionRule pr, boolean withForce) {
+ // BLOCK with force specified is a weaker rule than without.
+ return pr.getAction() == Action.BLOCK && (!pr.getForce() || withForce);
+ }
+
+ private PermissionRange toRange(String permissionName, boolean isChangeOwner) {
+ int blockAllowMin = Integer.MIN_VALUE, blockAllowMax = Integer.MAX_VALUE;
+
+ projectLoop:
+ for (List<Permission> ps : relevant.getBlockRules(permissionName)) {
+ boolean blockFound = false;
+ int projectBlockAllowMin = Integer.MIN_VALUE, projectBlockAllowMax = Integer.MAX_VALUE;
+
+ for (Permission p : ps) {
+ if (p.getExclusiveGroup()) {
+ for (PermissionRule pr : p.getRules()) {
+ if (pr.getAction() == Action.ALLOW && projectControl.match(pr, isChangeOwner)) {
+ // exclusive override, usually for a more specific ref.
+ continue projectLoop;
+ }
+ }
+ }
+
+ for (PermissionRule pr : p.getRules()) {
+ if (pr.getAction() == Action.BLOCK && projectControl.match(pr, isChangeOwner)) {
+ projectBlockAllowMin = pr.getMin() + 1;
+ projectBlockAllowMax = pr.getMax() - 1;
+ blockFound = true;
+ }
+ }
+
+ if (blockFound) {
+ for (PermissionRule pr : p.getRules()) {
+ if (pr.getAction() == Action.ALLOW && projectControl.match(pr, isChangeOwner)) {
+ projectBlockAllowMin = pr.getMin();
+ projectBlockAllowMax = pr.getMax();
+ break;
+ }
+ }
+ break;
+ }
+ }
+
+ blockAllowMin = Math.max(projectBlockAllowMin, blockAllowMin);
+ blockAllowMax = Math.min(projectBlockAllowMax, blockAllowMax);
+ }
+
+ int voteMin = 0, voteMax = 0;
+ for (PermissionRule pr : relevant.getAllowRules(permissionName)) {
+ if (pr.getAction() == PermissionRule.Action.ALLOW
+ && projectControl.match(pr, isChangeOwner)) {
+ // For votes, contrary to normal permissions, we aggregate all applicable rules.
+ voteMin = Math.min(voteMin, pr.getMin());
+ voteMax = Math.max(voteMax, pr.getMax());
+ }
+ }
+
+ return new PermissionRange(
+ permissionName, Math.max(voteMin, blockAllowMin), Math.min(voteMax, blockAllowMax));
+ }
+
+ private boolean isBlocked(String permissionName, boolean isChangeOwner, boolean withForce) {
+ // Permissions are ordered by (more general project, more specific ref). Because Permission
+ // does not have back pointers, we can't tell what ref-pattern or project each permission comes
+ // from.
+ List<List<Permission>> downwardPerProject = relevant.getBlockRules(permissionName);
+
+ projectLoop:
+ for (List<Permission> projectRules : downwardPerProject) {
+ boolean overrideFound = false;
+ for (Permission p : projectRules) {
+ // If this is an exclusive ALLOW, then block rules from the same project are ignored.
+ if (p.getExclusiveGroup()) {
+ for (PermissionRule pr : p.getRules()) {
+ if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
+ overrideFound = true;
+ break;
+ }
+ }
+ }
+ if (overrideFound) {
+ // Found an exclusive override, nothing further to do in this project.
+ continue projectLoop;
+ }
+
+ boolean blocked = false;
+ for (PermissionRule pr : p.getRules()) {
+ if (!withForce && pr.getForce()) {
+ // force on block rule only applies to withForce permission.
+ continue;
+ }
+
+ if (isBlock(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
+ blocked = true;
+ break;
+ }
+ }
+
+ if (blocked) {
+ // ALLOW in the same AccessSection (ie. in the same Permission) overrides the BLOCK.
+ for (PermissionRule pr : p.getRules()) {
+ if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
+ blocked = false;
+ break;
+ }
+ }
+ }
+
+ if (blocked) {
+ return true;
+ }
+ }
+ }
+
+ return false;
+ }
+
+ /** True if the user has this permission. */
+ private boolean canPerform(String permissionName, boolean isChangeOwner, boolean withForce) {
+ if (isBlocked(permissionName, isChangeOwner, withForce)) {
+ logger.atFine().log(
+ "'%s' cannot perform '%s' with force=%s on project '%s' for ref '%s' (caller: %s)"
+ + " because this permission is blocked",
+ getUser().getLoggableName(),
+ permissionName,
+ withForce,
+ projectControl.getProject().getName(),
+ refName,
+ callerFinder.findCaller());
+ return false;
+ }
+
+ for (PermissionRule pr : relevant.getAllowRules(permissionName)) {
+ if (isAllow(pr, withForce) && projectControl.match(pr, isChangeOwner)) {
+ logger.atFine().log(
+ "'%s' can perform '%s' with force=%s on project '%s' for ref '%s' (caller: %s)",
+ getUser().getLoggableName(),
+ permissionName,
+ withForce,
+ projectControl.getProject().getName(),
+ refName,
+ callerFinder.findCaller());
+ return true;
+ }
+ }
+
+ logger.atFine().log(
+ "'%s' cannot perform '%s' with force=%s on project '%s' for ref '%s' (caller: %s)",
+ getUser().getLoggableName(),
+ permissionName,
+ withForce,
+ projectControl.getProject().getName(),
+ refName,
+ callerFinder.findCaller());
+ return false;
+ }
+
+ private class ForRefImpl extends ForRef {
+ private String resourcePath;
+
+ @Override
+ public String resourcePath() {
+ if (resourcePath == null) {
+ resourcePath =
+ String.format(
+ "/projects/%s/+refs/%s", getProjectControl().getProjectState().getName(), refName);
+ }
+ return resourcePath;
+ }
+
+ @Override
+ public ForChange change(ChangeData cd) {
+ try {
+ // TODO(hiesel) Force callers to call database() and use db instead of cd.db()
+ return getProjectControl()
+ .controlFor(cd.db(), cd.change())
+ .asForChange(cd, Providers.of(cd.db()));
+ } catch (OrmException e) {
+ return FailedPermissionBackend.change("unavailable", e);
+ }
+ }
+
+ @Override
+ public ForChange change(ChangeNotes notes) {
+ Project.NameKey project = getProjectControl().getProject().getNameKey();
+ Change change = notes.getChange();
+ checkArgument(
+ project.equals(change.getProject()),
+ "expected change in project %s, not %s",
+ project,
+ change.getProject());
+ return getProjectControl().controlFor(notes).asForChange(null, db);
+ }
+
+ @Override
+ public ForChange indexedChange(ChangeData cd, ChangeNotes notes) {
+ return getProjectControl().controlFor(notes).asForChange(cd, db);
+ }
+
+ @Override
+ public void check(RefPermission perm) throws AuthException, PermissionBackendException {
+ if (!can(perm)) {
+ PermissionDeniedException pde = new PermissionDeniedException(perm, refName);
+ switch (perm) {
+ case UPDATE:
+ if (refName.equals(RefNames.REFS_CONFIG)) {
+ pde.setAdvice(
+ "Configuration changes can only be pushed by project owners\n"
+ + "who also have 'Push' rights on "
+ + RefNames.REFS_CONFIG);
+ } else {
+ pde.setAdvice("To push into this reference you need 'Push' rights.");
+ }
+ break;
+ case DELETE:
+ pde.setAdvice(
+ "You need 'Delete Reference' rights or 'Push' rights with the \n"
+ + "'Force Push' flag set to delete references.");
+ break;
+ case CREATE_CHANGE:
+ // This is misleading in the default permission backend, since "create change" on a
+ // branch is encoded as "push" on refs/for/DESTINATION.
+ pde.setAdvice(
+ "You need 'Create Change' rights to upload code review requests.\n"
+ + "Verify that you are pushing to the right branch.");
+ break;
+ case CREATE:
+ pde.setAdvice("You need 'Create' rights to create new references.");
+ break;
+ case CREATE_SIGNED_TAG:
+ pde.setAdvice("You need 'Create Signed Tag' rights to push a signed tag.");
+ break;
+ case CREATE_TAG:
+ pde.setAdvice("You need 'Create Tag' rights to push a normal tag.");
+ break;
+ case FORCE_UPDATE:
+ pde.setAdvice(
+ "You need 'Push' rights with 'Force' flag set to do a non-fastforward push.");
+ break;
+ case FORGE_AUTHOR:
+ pde.setAdvice(
+ "You need 'Forge Author' rights to push commits with another user as author.");
+ break;
+ case FORGE_COMMITTER:
+ pde.setAdvice(
+ "You need 'Forge Committer' rights to push commits with another user as committer.");
+ break;
+ case FORGE_SERVER:
+ pde.setAdvice(
+ "You need 'Forge Server' rights to push merge commits authored by the server.");
+ break;
+ case MERGE:
+ pde.setAdvice(
+ "You need 'Push Merge' in addition to 'Push' rights to push merge commits.");
+ break;
+
+ case READ:
+ pde.setAdvice("You need 'Read' rights to fetch or clone this ref.");
+ break;
+
+ case READ_CONFIG:
+ pde.setAdvice("You need 'Read' rights on refs/meta/config to see the configuration.");
+ break;
+ case READ_PRIVATE_CHANGES:
+ pde.setAdvice("You need 'Read Private Changes' to see private changes.");
+ break;
+ case SET_HEAD:
+ pde.setAdvice("You need 'Set HEAD' rights to set the default branch.");
+ break;
+ case SKIP_VALIDATION:
+ pde.setAdvice(
+ "You need 'Forge Author', 'Forge Server', 'Forge Committer'\n"
+ + "and 'Push Merge' rights to skip validation.");
+ break;
+ case UPDATE_BY_SUBMIT:
+ pde.setAdvice(
+ "You need 'Submit' rights on refs/for/ to submit changes during change upload.");
+ break;
+
+ case WRITE_CONFIG:
+ pde.setAdvice("You need 'Write' rights on refs/meta/config.");
+ break;
+ }
+ throw pde;
+ }
+ }
+
+ @Override
+ public Set<RefPermission> test(Collection<RefPermission> permSet)
+ throws PermissionBackendException {
+ EnumSet<RefPermission> ok = EnumSet.noneOf(RefPermission.class);
+ for (RefPermission perm : permSet) {
+ if (can(perm)) {
+ ok.add(perm);
+ }
+ }
+ return ok;
+ }
+
+ @Override
+ public BooleanCondition testCond(RefPermission perm) {
+ return new PermissionBackendCondition.ForRef(this, perm, getUser());
+ }
+
+ private boolean can(RefPermission perm) throws PermissionBackendException {
+ switch (perm) {
+ case READ:
+ return isVisible();
+ case CREATE:
+ // TODO This isn't an accurate test.
+ return canPerform(refPermissionName(perm));
+ case DELETE:
+ return canDelete();
+ case UPDATE:
+ return canUpdate();
+ case FORCE_UPDATE:
+ return canForceUpdate();
+ case SET_HEAD:
+ return projectControl.isOwner();
+
+ case FORGE_AUTHOR:
+ return canForgeAuthor();
+ case FORGE_COMMITTER:
+ return canForgeCommitter();
+ case FORGE_SERVER:
+ return canForgeGerritServerIdentity();
+ case MERGE:
+ return canUploadMerges();
+
+ case CREATE_CHANGE:
+ return canUpload();
+
+ case CREATE_TAG:
+ case CREATE_SIGNED_TAG:
+ return canPerform(refPermissionName(perm));
+
+ case UPDATE_BY_SUBMIT:
+ return projectControl.controlForRef(MagicBranch.NEW_CHANGE + refName).canSubmit(true);
+
+ case READ_PRIVATE_CHANGES:
+ return canPerform(Permission.VIEW_PRIVATE_CHANGES);
+
+ case READ_CONFIG:
+ return projectControl
+ .controlForRef(RefNames.REFS_CONFIG)
+ .canPerform(RefPermission.READ.name());
+ case WRITE_CONFIG:
+ return isOwner();
+
+ case SKIP_VALIDATION:
+ return canForgeAuthor()
+ && canForgeCommitter()
+ && canForgeGerritServerIdentity()
+ && canUploadMerges();
+ }
+ throw new PermissionBackendException(perm + " unsupported");
+ }
+ }
+
+ private static String refPermissionName(RefPermission refPermission) {
+ // Within this class, it's programmer error to call this method on a
+ // RefPermission that isn't associated with a permission name.
+ return DefaultPermissionMappings.refPermissionName(refPermission)
+ .orElseThrow(() -> new IllegalStateException("no name for " + refPermission));
+ }
+}