Change permissions to be branch based

The RefControl class introduces per-branch access control rules.
ProjectRight is replaced by RefRight in the database, shifting all
current access records to include a reference pattern that matches
the previously assumed target namespace.  For example, PUSH_HEAD
is now matched against refs/heads/*, as is SUBMIT.

Although this implementation starts the foundation for per-branch
level READ access, it is not fully supported.  The Git native
protocol exposes all branches to readers, which means users can
still fetch the Git objects even if the web UI wouldn't allow them
to see the change.

This work was a joint effort between Nico and Shawn.  Nico started
the change and did the bulk of the implementation.  Shawn did a
bunch of cleanup work near the tail end.  Consequently all bugs
are Shawn's fault.

Bug: issue 60
Change-Id: I62401d80cbb885180614a4f20a945f5611de8986
Signed-off-by: Shawn O. Pearce <sop@google.com>
This commit is contained in:
Nico Sallembien
2010-01-25 09:05:17 -08:00
committed by Shawn O. Pearce
parent 55f3363fce
commit 75afdfdc84
33 changed files with 901 additions and 440 deletions

View File

@@ -14,13 +14,6 @@
package com.google.gerrit.server.git;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD_REPLACE;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD_UPDATE;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_TAG;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_TAG_ANNOTATED;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_TAG_ANY;
import com.google.gerrit.common.ChangeHookRunner;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.ApprovalType;
@@ -55,6 +48,7 @@ import com.google.gerrit.server.mail.MergedSender;
import com.google.gerrit.server.mail.ReplacePatchSetSender;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.RefControl;
import com.google.gwtorm.client.AtomicUpdate;
import com.google.gwtorm.client.OrmException;
import com.google.inject.Inject;
@@ -74,14 +68,12 @@ import org.eclipse.jgit.revwalk.RevFlag;
import org.eclipse.jgit.revwalk.RevFlagSet;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevSort;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevWalk;
import org.eclipse.jgit.transport.PostReceiveHook;
import org.eclipse.jgit.transport.PreReceiveHook;
import org.eclipse.jgit.transport.ReceiveCommand;
import org.eclipse.jgit.transport.ReceivePack;
import org.eclipse.jgit.transport.ReceiveCommand.Result;
import org.eclipse.jgit.transport.ReceiveCommand.Type;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
@@ -234,7 +226,7 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
/** Determine if the user can upload commits. */
public Capable canUpload() {
if (!projectControl.canPerform(ApprovalCategory.READ, (short) 2)) {
if (!projectControl.canUploadToAtLeastOneRef()) {
String reqName = project.getName();
return new Capable("Upload denied for project '" + reqName + "'");
}
@@ -444,8 +436,11 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
continue;
case DELETE:
parseDelete(cmd);
continue;
case UPDATE_NONFASTFORWARD:
parseRewindOrDelete(cmd);
parseRewind(cmd);
continue;
}
@@ -456,88 +451,71 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
}
private void parseCreate(final ReceiveCommand cmd) {
if (projectControl.canCreateRef(cmd.getRefName())) {
if (isTag(cmd)) {
parseCreateTag(cmd);
}
final RevObject obj;
try {
obj = rp.getRevWalk().parseAny(cmd.getNewId());
} catch (IOException err) {
log.error("Invalid object " + cmd.getNewId().name() + " for "
+ cmd.getRefName() + " creation", err);
reject(cmd, "invalid object");
return;
}
RefControl ctl = projectControl.controlForRef(cmd.getRefName());
if (ctl.canCreate(rp.getRevWalk(), obj)) {
// Let the core receive process handle it
} else {
reject(cmd);
}
}
private void parseCreateTag(final ReceiveCommand cmd) {
try {
final RevObject obj = rp.getRevWalk().parseAny(cmd.getNewId());
if (!(obj instanceof RevTag)) {
reject(cmd, "not annotated tag");
return;
}
if (canPerform(PUSH_TAG, PUSH_TAG_ANY)) {
// If we can push any tag, validation is sufficient at this point.
//
return;
}
final RevTag tag = (RevTag) obj;
final PersonIdent tagger = tag.getTaggerIdent();
if (tagger == null) {
reject(cmd, "no tagger");
return;
}
final String email = tagger.getEmailAddress();
if (!currentUser.getEmailAddresses().contains(email)) {
reject(cmd, "invalid tagger " + email);
return;
}
if (tag.getFullMessage().contains("-----BEGIN PGP SIGNATURE-----\n")) {
// Signed tags are currently assumed valid, as we don't have a GnuPG
// key ring to validate them against, and we might be missing the
// necessary (but currently optional) BouncyCastle Crypto libraries.
//
} else if (canPerform(PUSH_TAG, PUSH_TAG_ANNOTATED)) {
// User is permitted to push an unsigned annotated tag.
//
} else {
reject(cmd, "must be signed");
return;
}
// Let the core receive process handle it
//
} catch (IOException e) {
log.error("Bad tag " + cmd.getRefName() + " " + cmd.getNewId().name(), e);
reject(cmd, "invalid object");
}
}
private void parseUpdate(final ReceiveCommand cmd) {
if (isHead(cmd) && canPerform(PUSH_HEAD, PUSH_HEAD_UPDATE)) {
RefControl ctl = projectControl.controlForRef(cmd.getRefName());
if (ctl.canUpdate()) {
// Let the core receive process handle it
} else {
reject(cmd);
}
}
private void parseRewindOrDelete(final ReceiveCommand cmd) {
if (isHead(cmd) && cmd.getType() == Type.DELETE
&& projectControl.canDeleteRef(cmd.getRefName())) {
private void parseDelete(final ReceiveCommand cmd) {
RefControl ctl = projectControl.controlForRef(cmd.getRefName());
if (ctl.canDelete()) {
// Let the core receive process handle it
} else if (isHead(cmd) && canPerform(PUSH_HEAD, PUSH_HEAD_REPLACE)) {
// Let the core receive process handle it
} else if (isHead(cmd) && cmd.getType() == Type.UPDATE_NONFASTFORWARD) {
cmd.setResult(ReceiveCommand.Result.REJECTED_NONFASTFORWARD);
} else {
reject(cmd);
}
}
private void parseRewind(final ReceiveCommand cmd) {
final RevObject oldObject, newObject;
try {
oldObject = rp.getRevWalk().parseAny(cmd.getOldId());
} catch (IOException err) {
log.error("Invalid object " + cmd.getOldId().name() + " for "
+ cmd.getRefName() + " forced update", err);
reject(cmd, "invalid object");
return;
}
try {
newObject = rp.getRevWalk().parseAny(cmd.getNewId());
} catch (IOException err) {
log.error("Invalid object " + cmd.getNewId().name() + " for "
+ cmd.getRefName() + " forced update", err);
reject(cmd, "invalid object");
return;
}
RefControl ctl = projectControl.controlForRef(cmd.getRefName());
if (oldObject instanceof RevCommit && newObject instanceof RevCommit
&& ctl.canForceUpdate()) {
// Let the core receive process handle it
} else {
cmd.setResult(ReceiveCommand.Result.REJECTED_NONFASTFORWARD);
}
}
private void parseNewChangeCommand(final ReceiveCommand cmd) {
// Permit exactly one new change request per push.
//
@@ -584,6 +562,10 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
return;
}
if (!projectControl.controlForRef(destBranch).canUpload()) {
reject(cmd);
}
// Validate that the new commits are connected with the existing heads
// or tags of this repository. If they aren't, we want to abort. We do
// this check by coloring the tip CONNECTED and letting a RevWalk push
@@ -957,6 +939,10 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
reject(request.cmd, "change " + request.ontoChange + " closed");
return null;
}
if (!projectControl.controlFor(change).canAddPatchSet()) {
reject(request.cmd, "cannot replace " + request.ontoChange);
return null;
}
final PatchSet.Id priorPatchSet = change.currentPatchSetId();
for (final PatchSet ps : db.patchSets().byChange(request.ontoChange)) {
@@ -1488,10 +1474,6 @@ public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
return new RevId(src.getId().name());
}
private boolean canPerform(final ApprovalCategory.Id actionId, final short val) {
return projectControl.canPerform(actionId, val);
}
private static void reject(final ReceiveCommand cmd) {
reject(cmd, "prohibited by Gerrit");
}

View File

@@ -78,28 +78,32 @@ public class ChangeControl {
}
}
private final ProjectControl projectControl;
private final RefControl refControl;
private final Change change;
ChangeControl(final ProjectControl p, final Change c) {
this.projectControl = p;
ChangeControl(final RefControl r, final Change c) {
this.refControl = r;
this.change = c;
}
public ChangeControl forAnonymousUser() {
return new ChangeControl(projectControl.forAnonymousUser(), change);
return new ChangeControl(getRefControl().forAnonymousUser(), getChange());
}
public ChangeControl forUser(final CurrentUser who) {
return new ChangeControl(projectControl.forUser(who), change);
return new ChangeControl(getRefControl().forUser(who), getChange());
}
public RefControl getRefControl() {
return refControl;
}
public CurrentUser getCurrentUser() {
return getProjectControl().getCurrentUser();
return getRefControl().getCurrentUser();
}
public ProjectControl getProjectControl() {
return projectControl;
return getRefControl().getProjectControl();
}
public Project getProject() {
@@ -112,17 +116,23 @@ public class ChangeControl {
/** Can this user see this change? */
public boolean isVisible() {
return getProjectControl().isVisible();
return getRefControl().isVisible();
}
/** Can this user abandon this change? */
public boolean canAbandon() {
return isOwner() // owner (aka creator) of the change can abandon
|| getRefControl().isOwner() // branch owner can abandon
|| getProjectControl().isOwner() // project owner can abandon
|| getCurrentUser().isAdministrator() // site administers are god
;
}
/** Can this user add a patch set to this change? */
public boolean canAddPatchSet() {
return getRefControl().canUpload();
}
/** Is this user the owner of the change? */
public boolean isOwner() {
if (getCurrentUser() instanceof IdentifiedUser) {

View File

@@ -15,7 +15,7 @@
package com.google.gerrit.server.project;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.ProjectRight;
import com.google.gerrit.reviewdb.RefRight;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.server.cache.Cache;
import com.google.gerrit.server.cache.CacheModule;
@@ -76,7 +76,7 @@ public class ProjectCacheImpl implements ProjectCache {
this.inheritedRights = new ProjectState.InheritedRights() {
@Override
public Collection<ProjectRight> get() {
public Collection<RefRight> get() {
return ProjectCacheImpl.this.get(wildProject).getLocalRights();
}
};
@@ -90,8 +90,8 @@ public class ProjectCacheImpl implements ProjectCache {
return null;
}
final Collection<ProjectRight> rights =
Collections.unmodifiableCollection(db.projectRights().byProject(
final Collection<RefRight> rights =
Collections.unmodifiableCollection(db.refRights().byProject(
p.getNameKey()).toList());
return projectStateFactory.create(p, rights, inheritedRights);

View File

@@ -14,22 +14,16 @@
package com.google.gerrit.server.project;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD_CREATE;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD_REPLACE;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_TAG;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.ApprovalCategory;
import com.google.gerrit.reviewdb.Branch;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.ProjectRight;
import com.google.gerrit.reviewdb.RefRight;
import com.google.gerrit.server.CurrentUser;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.lib.Constants;
import java.util.Set;
/** Access control management for a user accessing a project's data. */
@@ -96,7 +90,15 @@ public class ProjectControl {
}
public ChangeControl controlFor(final Change change) {
return new ChangeControl(this, change);
return new ChangeControl(controlForRef(change.getDest()), change);
}
public RefControl controlForRef(Branch.NameKey ref) {
return controlForRef(ref.get());
}
public RefControl controlForRef(String refName) {
return new RefControl(this, refName);
}
public CurrentUser getCurrentUser() {
@@ -113,62 +115,28 @@ public class ProjectControl {
/** Can this user see this project exists? */
public boolean isVisible() {
return canPerform(ApprovalCategory.READ, (short) 1);
return canPerformOnAnyRef(ApprovalCategory.READ, (short) 1);
}
/** Is this user a project owner? Ownership does not imply {@link #isVisible()} */
public boolean isOwner() {
return canPerform(ApprovalCategory.OWN, (short) 1)
return canPerformOnAllRefs(ApprovalCategory.OWN, (short) 1)
|| getCurrentUser().isAdministrator();
}
/** Can this user create the given ref through this access path? */
public boolean canCreateRef(final String refname) {
switch (user.getAccessPath()) {
case WEB:
if (isOwner()) {
return true;
}
if (isHead(refname) && canPerform(PUSH_HEAD, PUSH_HEAD_CREATE)) {
return true;
}
return false;
case SSH:
if (isHead(refname) && canPerform(PUSH_HEAD, PUSH_HEAD_CREATE)) {
return true;
}
if (isTag(refname) && canPerform(PUSH_TAG, (short) 1)) {
return true;
}
return false;
default:
return false;
}
/** @return true if the user can upload to at least one reference */
public boolean canUploadToAtLeastOneRef() {
return canPerformOnAnyRef(ApprovalCategory.READ, (short) 2);
}
/** Can this user delete the given ref through this access path? */
public boolean canDeleteRef(final String refname) {
switch (user.getAccessPath()) {
case WEB:
if (isOwner()) {
return true;
}
if (isHead(refname) && canPerform(PUSH_HEAD, PUSH_HEAD_REPLACE)) {
return true;
}
return false;
private boolean canPerformOnAnyRef(ApprovalCategory.Id actionId,
short requireValue) {
return canPerform(actionId, requireValue, null /* any ref */);
}
case SSH:
if (isHead(refname) && canPerform(PUSH_HEAD, PUSH_HEAD_REPLACE)) {
return true;
}
return false;
default:
return false;
}
private boolean canPerformOnAllRefs(ApprovalCategory.Id actionId,
short requireValue) {
return canPerform(actionId, requireValue, "refs/*");
}
/**
@@ -181,15 +149,19 @@ public class ProjectControl {
* @param actionId unique action id.
* @param requireValue minimum value the application needs to perform this
* action.
* @param refPattern if null, matches any RefRight, otherwise matches only
* those RefRights with the pattern exactly equal to the input.
* @return true if the action can be performed; false if the user lacks the
* necessary permission.
*/
public boolean canPerform(final ApprovalCategory.Id actionId,
final short requireValue) {
private boolean canPerform(final ApprovalCategory.Id actionId,
final short requireValue, final String refPattern) {
final Set<AccountGroup.Id> groups = user.getEffectiveGroups();
int val = Integer.MIN_VALUE;
for (final ProjectRight pr : state.getLocalRights()) {
for (final RefRight pr : state.getLocalRights()) {
if (actionId.equals(pr.getApprovalCategoryId())
&& (refPattern == null || refPattern.equals(pr.getRefPattern()))
&& groups.contains(pr.getAccountGroupId())) {
if (val < 0 && pr.getMaxValue() > 0) {
// If one of the user's groups had denied them access, but
@@ -205,9 +177,11 @@ public class ProjectControl {
}
}
}
if (val == Integer.MIN_VALUE && actionId.canInheritFromWildProject()) {
for (final ProjectRight pr : state.getInheritedRights()) {
for (final RefRight pr : state.getInheritedRights()) {
if (actionId.equals(pr.getApprovalCategoryId())
&& (refPattern == null || refPattern.equals(pr.getRefPattern()))
&& groups.contains(pr.getAccountGroupId())) {
val = Math.max(pr.getMaxValue(), val);
}
@@ -216,12 +190,4 @@ public class ProjectControl {
return val >= requireValue;
}
private static boolean isHead(final String refname) {
return refname.startsWith(Constants.R_HEADS);
}
private static boolean isTag(final String refname) {
return refname.startsWith(Constants.R_TAGS);
}
}

View File

@@ -17,7 +17,7 @@ package com.google.gerrit.server.project;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.ApprovalCategory;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.ProjectRight;
import com.google.gerrit.reviewdb.RefRight;
import com.google.gerrit.server.AnonymousUser;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.config.WildProjectName;
@@ -32,19 +32,19 @@ import java.util.Set;
/** Cached information on a project. */
public class ProjectState {
public interface Factory {
ProjectState create(Project project, Collection<ProjectRight> localRights,
ProjectState create(Project project, Collection<RefRight> localRights,
InheritedRights inheritedRights);
}
public interface InheritedRights {
Collection<ProjectRight> get();
Collection<RefRight> get();
}
private final AnonymousUser anonymousUser;
private final Project.NameKey wildProject;
private final Project project;
private final Collection<ProjectRight> localRights;
private final Collection<RefRight> localRights;
private final InheritedRights inheritedRights;
private final Set<AccountGroup.Id> owners;
@@ -52,7 +52,7 @@ public class ProjectState {
protected ProjectState(final AnonymousUser anonymousUser,
@WildProjectName final Project.NameKey wildProject,
@Assisted final Project project,
@Assisted final Collection<ProjectRight> rights,
@Assisted final Collection<RefRight> rights,
@Assisted final InheritedRights inheritedRights) {
this.anonymousUser = anonymousUser;
this.wildProject = wildProject;
@@ -62,7 +62,7 @@ public class ProjectState {
this.inheritedRights = inheritedRights;
final HashSet<AccountGroup.Id> groups = new HashSet<AccountGroup.Id>();
for (final ProjectRight right : rights) {
for (final RefRight right : rights) {
if (ApprovalCategory.OWN.equals(right.getApprovalCategoryId())
&& right.getMaxValue() > 0) {
groups.add(right.getAccountGroupId());
@@ -76,12 +76,12 @@ public class ProjectState {
}
/** Get the rights that pertain only to this project. */
public Collection<ProjectRight> getLocalRights() {
public Collection<RefRight> getLocalRights() {
return localRights;
}
/** Get the rights this project inherits from the wild project. */
public Collection<ProjectRight> getInheritedRights() {
public Collection<RefRight> getInheritedRights() {
if (isSpecialWildProject()) {
return Collections.emptyList();
}

View File

@@ -0,0 +1,243 @@
// 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.project;
import static com.google.gerrit.reviewdb.ApprovalCategory.OWN;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD_CREATE;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD_REPLACE;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD_UPDATE;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_TAG;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_TAG_ANNOTATED;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_TAG_ANY;
import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_TAG_SIGNED;
import static com.google.gerrit.reviewdb.ApprovalCategory.READ;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.ApprovalCategory;
import com.google.gerrit.reviewdb.RefRight;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevObject;
import org.eclipse.jgit.revwalk.RevTag;
import org.eclipse.jgit.revwalk.RevWalk;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Set;
/** Manages access control for Git references (aka branches, tags). */
public class RefControl {
private final ProjectControl projectControl;
private final String refName;
RefControl(final ProjectControl projectControl, final String refName) {
this.projectControl = projectControl;
this.refName = refName;
}
public String getRefName() {
return refName;
}
public ProjectControl getProjectControl() {
return projectControl;
}
public CurrentUser getCurrentUser() {
return getProjectControl().getCurrentUser();
}
public RefControl forAnonymousUser() {
return getProjectControl().forAnonymousUser().controlForRef(getRefName());
}
public RefControl forUser(final CurrentUser who) {
return getProjectControl().forUser(who).controlForRef(getRefName());
}
/** Is this user a ref owner? */
public boolean isOwner() {
return canPerform(OWN, (short) 1) || getProjectControl().isOwner();
}
/** Can this user see this reference exists? */
public boolean isVisible() {
return canPerform(READ, (short) 1);
}
/**
* Determines whether the user can upload a change to the ref controlled by
* this object.
*
* @return {@code true} if the user specified can upload a change to the Git
* ref
*/
public boolean canUpload() {
return canPerform(READ, (short) 2);
}
/** @return true if the user can update the reference as a fast-forward. */
public boolean canUpdate() {
return canPerform(PUSH_HEAD, PUSH_HEAD_UPDATE);
}
/** @return true if the user can rewind (force push) the reference. */
public boolean canForceUpdate() {
return canPerform(PUSH_HEAD, PUSH_HEAD_REPLACE) || canDelete();
}
/**
* Determines whether the user can create a new Git ref.
*
* @param rw revision pool {@code object} was parsed in.
* @param object the object the user will start the reference with.
* @return {@code true} if the user specified can create a new Git ref
*/
public boolean canCreate(RevWalk rw, RevObject object) {
boolean owner;
switch (getCurrentUser().getAccessPath()) {
case WEB:
owner = isOwner();
break;
default:
owner = false;
}
if (object instanceof RevCommit) {
return owner || canPerform(PUSH_HEAD, PUSH_HEAD_CREATE);
} else if (object instanceof RevTag) {
try {
rw.parseBody(object);
} catch (IOException e) {
return false;
}
final RevTag tag = (RevTag) object;
// Require the tagger to be present and match the current user's
// email address, unless PUSH_ANY_TAG was granted.
//
final PersonIdent tagger = tag.getTaggerIdent();
if (tagger == null || !(getCurrentUser() instanceof IdentifiedUser)) {
return owner || canPerform(PUSH_TAG, PUSH_TAG_ANY);
}
final IdentifiedUser user = (IdentifiedUser) getCurrentUser();
if (!user.getEmailAddresses().contains(tagger.getEmailAddress())) {
return owner || canPerform(PUSH_TAG, PUSH_TAG_ANY);
}
// If the tag has a PGP signature, allow a lower level of permission
// than if it doesn't have a PGP signature.
//
if (tag.getFullMessage().contains("-----BEGIN PGP SIGNATURE-----\n")) {
return owner || canPerform(PUSH_TAG, PUSH_TAG_SIGNED);
} else {
return owner || canPerform(PUSH_TAG, PUSH_TAG_ANNOTATED);
}
} else {
return false;
}
}
/**
* 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.
*/
public boolean canDelete() {
switch (getCurrentUser().getAccessPath()) {
case WEB:
return isOwner() || canPerform(PUSH_HEAD, PUSH_HEAD_REPLACE);
case SSH:
return canPerform(PUSH_HEAD, PUSH_HEAD_REPLACE);
default:
return false;
}
}
private boolean canPerform(ApprovalCategory.Id actionId, short level) {
final Set<AccountGroup.Id> groups = getCurrentUser().getEffectiveGroups();
int val = Integer.MIN_VALUE;
for (final RefRight right : getLocalRights()) {
if (right.getApprovalCategoryId().equals(actionId)
&& groups.contains(right.getAccountGroupId())) {
if (val < 0 && right.getMaxValue() > 0) {
// If one of the user's groups had denied them access, but
// this group grants them access, prefer the grant over
// the denial. We have to break the tie somehow and we
// prefer being "more open" to being "more closed".
//
val = right.getMaxValue();
} else {
// Otherwise we use the largest value we can get.
//
val = Math.max(right.getMaxValue(), val);
}
}
}
if (val == Integer.MIN_VALUE && actionId.canInheritFromWildProject()) {
for (final RefRight pr : getInheritedRights()) {
if (actionId.equals(pr.getApprovalCategoryId())
&& groups.contains(pr.getAccountGroupId())) {
val = Math.max(pr.getMaxValue(), val);
}
}
}
return val >= level;
}
private Collection<RefRight> getLocalRights() {
return filter(projectControl.getProjectState().getLocalRights());
}
private Collection<RefRight> getInheritedRights() {
return filter(projectControl.getProjectState().getInheritedRights());
}
private Collection<RefRight> filter(Collection<RefRight> all) {
List<RefRight> mine = new ArrayList<RefRight>(all.size());
for (RefRight right : all) {
if (matches(getRefName(), right.getRefPattern())) {
mine.add(right);
}
}
return mine;
}
public static boolean matches(String refName, String refPattern) {
if (refPattern.endsWith("/*")) {
String prefix = refPattern.substring(0, refPattern.length() - 1);
return refName.startsWith(prefix);
} else {
return refName.equals(refPattern);
}
}
}

View File

@@ -19,7 +19,7 @@ import com.google.gerrit.reviewdb.ApprovalCategory;
import com.google.gerrit.reviewdb.ApprovalCategoryValue;
import com.google.gerrit.reviewdb.CurrentSchemaVersion;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.ProjectRight;
import com.google.gerrit.reviewdb.RefRight;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gerrit.reviewdb.SystemConfig;
import com.google.gerrit.server.config.SitePath;
@@ -189,12 +189,13 @@ public class SchemaCreator {
c.approvalCategories().insert(Collections.singleton(cat));
c.approvalCategoryValues().insert(vals);
final ProjectRight approve =
new ProjectRight(new ProjectRight.Key(DEFAULT_WILD_NAME, cat.getId(),
final RefRight approve =
new RefRight(new RefRight.Key(DEFAULT_WILD_NAME,
new RefRight.RefPattern("refs/heads/*"), cat.getId(),
sConfig.registeredGroupId));
approve.setMaxValue((short) 1);
approve.setMinValue((short) -1);
c.projectRights().insert(Collections.singleton(approve));
c.refRights().insert(Collections.singleton(approve));
}
private void initOwnerCategory(final ReviewDb c) throws OrmException {
@@ -225,29 +226,30 @@ public class SchemaCreator {
c.approvalCategories().insert(Collections.singleton(cat));
c.approvalCategoryValues().insert(vals);
final RefRight.RefPattern pattern = new RefRight.RefPattern("refs/*");
{
final ProjectRight read =
new ProjectRight(new ProjectRight.Key(DEFAULT_WILD_NAME, cat.getId(),
sConfig.anonymousGroupId));
final RefRight read =
new RefRight(new RefRight.Key(DEFAULT_WILD_NAME, pattern,
cat.getId(), sConfig.anonymousGroupId));
read.setMaxValue((short) 1);
read.setMinValue((short) 1);
c.projectRights().insert(Collections.singleton(read));
c.refRights().insert(Collections.singleton(read));
}
{
final ProjectRight read =
new ProjectRight(new ProjectRight.Key(DEFAULT_WILD_NAME, cat.getId(),
sConfig.registeredGroupId));
final RefRight read =
new RefRight(new RefRight.Key(DEFAULT_WILD_NAME, pattern,
cat.getId(), sConfig.registeredGroupId));
read.setMaxValue((short) 2);
read.setMinValue((short) 1);
c.projectRights().insert(Collections.singleton(read));
c.refRights().insert(Collections.singleton(read));
}
{
final ProjectRight read =
new ProjectRight(new ProjectRight.Key(DEFAULT_WILD_NAME, cat.getId(),
sConfig.adminGroupId));
final RefRight read =
new RefRight(new RefRight.Key(DEFAULT_WILD_NAME, pattern,
cat.getId(), sConfig.adminGroupId));
read.setMaxValue((short) 1);
read.setMinValue((short) 1);
c.projectRights().insert(Collections.singleton(read));
c.refRights().insert(Collections.singleton(read));
}
}

View File

@@ -32,7 +32,7 @@ import java.util.List;
/** A version of the database schema. */
public abstract class SchemaVersion {
/** The current schema version. */
private static final Class<? extends SchemaVersion> C = Schema_24.class;
private static final Class<? extends SchemaVersion> C = Schema_25.class;
public static class Module extends AbstractModule {
@Override

View File

@@ -0,0 +1,110 @@
// 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.schema;
import com.google.gerrit.reviewdb.AccountGroup;
import com.google.gerrit.reviewdb.ApprovalCategory;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.RefRight;
import com.google.gerrit.reviewdb.ReviewDb;
import com.google.gwtorm.client.OrmException;
import com.google.gwtorm.jdbc.JdbcSchema;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
class Schema_25 extends SchemaVersion {
private Set<ApprovalCategory.Id> nonActions;
@Inject
Schema_25(Provider<Schema_24> prior) {
super(prior);
}
@Override
protected void migrateData(ReviewDb db) throws OrmException, SQLException {
nonActions = new HashSet<ApprovalCategory.Id>();
for (ApprovalCategory c : db.approvalCategories().all()) {
if (!c.isAction()) {
nonActions.add(c.getId());
}
}
List<RefRight> rights = new ArrayList<RefRight>();
Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
try {
ResultSet rs = stmt.executeQuery("SELECT * FROM project_rights");
try {
while (rs.next()) {
rights.add(toRefRight(rs));
}
} finally {
rs.close();
}
db.refRights().insert(rights);
stmt.execute("CREATE INDEX ref_rights_byCatGroup"
+ " ON ref_rights (category_id, group_id)");
} finally {
stmt.close();
}
}
private RefRight toRefRight(ResultSet rs) throws SQLException {
short min_value = rs.getShort("min_value");
short max_value = rs.getShort("max_value");
String category_id = rs.getString("category_id");
int group_id = rs.getInt("group_id");
String project_name = rs.getString("project_name");
ApprovalCategory.Id category = new ApprovalCategory.Id(category_id);
Project.NameKey project = new Project.NameKey(project_name);
AccountGroup.Id group = new AccountGroup.Id(group_id);
RefRight.RefPattern ref;
if (category.equals(ApprovalCategory.SUBMIT)
|| category.equals(ApprovalCategory.PUSH_HEAD)
|| nonActions.contains(category)) {
// Explicitly related to a branch head.
ref = new RefRight.RefPattern("refs/heads/*");
} else if (category.equals(ApprovalCategory.PUSH_TAG)) {
// Explicitly related to the tag namespace.
ref = new RefRight.RefPattern("refs/tags/*");
} else if (category.equals(ApprovalCategory.READ)
|| category.equals(ApprovalCategory.OWN)) {
// Currently these are project-wide rights, so apply that way.
ref = new RefRight.RefPattern("refs/*");
} else {
// Assume project wide for the default.
ref = new RefRight.RefPattern("refs/*");
}
RefRight.Key key = new RefRight.Key(project, ref, category, group);
RefRight r = new RefRight(key);
r.setMinValue(min_value);
r.setMaxValue(max_value);
return r;
}
}

View File

@@ -17,7 +17,7 @@ package com.google.gerrit.server.workflow;
import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.reviewdb.ApprovalCategory;
import com.google.gerrit.reviewdb.PatchSetApproval;
import com.google.gerrit.reviewdb.ProjectRight;
import com.google.gerrit.reviewdb.RefRight;
import com.google.gerrit.server.CurrentUser;
import java.util.HashMap;
@@ -89,7 +89,7 @@ public abstract class CategoryFunction {
public boolean isValid(final CurrentUser user, final ApprovalType at,
final FunctionState state) {
for (final ProjectRight pr : state.getAllRights(at)) {
for (final RefRight pr : state.getAllRights(at)) {
if (user.getEffectiveGroups().contains(pr.getAccountGroupId())
&& (pr.getMinValue() < 0 || pr.getMaxValue() > 0)) {
return true;

View File

@@ -23,12 +23,13 @@ import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.PatchSet;
import com.google.gerrit.reviewdb.PatchSetApproval;
import com.google.gerrit.reviewdb.Project;
import com.google.gerrit.reviewdb.ProjectRight;
import com.google.gerrit.reviewdb.RefRight;
import com.google.gerrit.reviewdb.ApprovalCategory.Id;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.GroupCache;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.project.RefControl;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
@@ -57,10 +58,10 @@ public class FunctionState {
new HashMap<ApprovalCategory.Id, Boolean>();
private final Change change;
private final ProjectState project;
private final Map<ApprovalCategory.Id, Collection<ProjectRight>> allRights =
new HashMap<ApprovalCategory.Id, Collection<ProjectRight>>();
private Map<ApprovalCategory.Id, Collection<ProjectRight>> projectRights;
private Map<ApprovalCategory.Id, Collection<ProjectRight>> inheritedRights;
private final Map<ApprovalCategory.Id, Collection<RefRight>> allRights =
new HashMap<ApprovalCategory.Id, Collection<RefRight>>();
private Map<ApprovalCategory.Id, Collection<RefRight>> RefRights;
private Map<ApprovalCategory.Id, Collection<RefRight>> inheritedRights;
private Set<PatchSetApproval> modified;
@Inject
@@ -135,39 +136,39 @@ public class FunctionState {
return Collections.emptySet();
}
public Collection<ProjectRight> getProjectRights(final ApprovalType at) {
return getProjectRights(id(at));
public Collection<RefRight> getRefRights(final ApprovalType at) {
return getRefRights(id(at));
}
public Collection<ProjectRight> getProjectRights(final ApprovalCategory.Id id) {
if (projectRights == null) {
projectRights = index(project.getLocalRights());
public Collection<RefRight> getRefRights(final ApprovalCategory.Id id) {
if (RefRights == null) {
RefRights = index(project.getLocalRights());
}
final Collection<ProjectRight> l = projectRights.get(id);
return l != null ? l : Collections.<ProjectRight> emptySet();
final Collection<RefRight> l = RefRights.get(id);
return l != null ? l : Collections.<RefRight> emptySet();
}
public Collection<ProjectRight> getWildcardRights(final ApprovalType at) {
public Collection<RefRight> getWildcardRights(final ApprovalType at) {
return getWildcardRights(id(at));
}
public Collection<ProjectRight> getWildcardRights(final ApprovalCategory.Id id) {
public Collection<RefRight> getWildcardRights(final ApprovalCategory.Id id) {
if (inheritedRights == null) {
inheritedRights = index(project.getInheritedRights());
}
final Collection<ProjectRight> l = inheritedRights.get(id);
return l != null ? l : Collections.<ProjectRight> emptySet();
final Collection<RefRight> l = inheritedRights.get(id);
return l != null ? l : Collections.<RefRight> emptySet();
}
public Collection<ProjectRight> getAllRights(final ApprovalType at) {
public Collection<RefRight> getAllRights(final ApprovalType at) {
return getAllRights(id(at));
}
public Collection<ProjectRight> getAllRights(final ApprovalCategory.Id id) {
Collection<ProjectRight> l = allRights.get(id);
public Collection<RefRight> getAllRights(final ApprovalCategory.Id id) {
Collection<RefRight> l = allRights.get(id);
if (l == null) {
l = new ArrayList<ProjectRight>();
l.addAll(getProjectRights(id));
l = new ArrayList<RefRight>();
l.addAll(getRefRights(id));
l.addAll(getWildcardRights(id));
l = Collections.unmodifiableCollection(l);
allRights.put(id, l);
@@ -175,18 +176,19 @@ public class FunctionState {
return l;
}
private static Map<Id, Collection<ProjectRight>> index(
final Collection<ProjectRight> rights) {
final HashMap<ApprovalCategory.Id, Collection<ProjectRight>> r;
private Map<Id, Collection<RefRight>> index(final Collection<RefRight> rights) {
final HashMap<ApprovalCategory.Id, Collection<RefRight>> r;
r = new HashMap<ApprovalCategory.Id, Collection<ProjectRight>>();
for (final ProjectRight pr : rights) {
Collection<ProjectRight> l = r.get(pr.getApprovalCategoryId());
if (l == null) {
l = new ArrayList<ProjectRight>();
r.put(pr.getApprovalCategoryId(), l);
r = new HashMap<ApprovalCategory.Id, Collection<RefRight>>();
for (final RefRight pr : rights) {
if (RefControl.matches(change.getDest().get(), pr.getRefPattern())) {
Collection<RefRight> l = r.get(pr.getApprovalCategoryId());
if (l == null) {
l = new ArrayList<RefRight>();
r.put(pr.getApprovalCategoryId(), l);
}
l.add(pr);
}
l.add(pr);
}
return r;
}
@@ -214,11 +216,11 @@ public class FunctionState {
/**
* Normalize the approval record to be inside the maximum range permitted by
* the ProjectRights granted to groups the account is a member of.
* the RefRights granted to groups the account is a member of.
* <p>
* If multiple ProjectRights are matched (assigned to different groups the
* account is a member of) the lowest minValue and the highest maxValue of the
* union of them is used.
* If multiple RefRights are matched (assigned to different groups the account
* is a member of) the lowest minValue and the highest maxValue of the union
* of them is used.
* <p>
* If the record's value was modified, its automatically marked as dirty.
*/
@@ -228,7 +230,7 @@ public class FunctionState {
// Find the maximal range actually granted to the user.
//
short minAllowed = 0, maxAllowed = 0;
for (final ProjectRight r : getAllRights(a.getCategoryId())) {
for (final RefRight r : getAllRights(a.getCategoryId())) {
final AccountGroup.Id grp = r.getAccountGroupId();
if (user.getEffectiveGroups().contains(grp)) {
minAllowed = (short) Math.min(minAllowed, r.getMinValue());

View File

@@ -17,7 +17,7 @@ package com.google.gerrit.server.workflow;
import com.google.gerrit.common.data.ApprovalType;
import com.google.gerrit.reviewdb.ApprovalCategory;
import com.google.gerrit.reviewdb.Change;
import com.google.gerrit.reviewdb.ProjectRight;
import com.google.gerrit.reviewdb.RefRight;
import com.google.gerrit.server.CurrentUser;
/**
@@ -42,7 +42,7 @@ public class SubmitFunction extends CategoryFunction {
public boolean isValid(final CurrentUser user, final ApprovalType at,
final FunctionState state) {
if (valid(at, state)) {
for (final ProjectRight pr : state.getAllRights(at)) {
for (final RefRight pr : state.getAllRights(at)) {
if (user.getEffectiveGroups().contains(pr.getAccountGroupId())
&& pr.getMaxValue() > 0) {
return true;