blob: ddfe752e92207a4a472e4e311e7dbf9b0ca708a1 [file] [log] [blame]
// Copyright (C) 2009 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 org.eclipse.jgit.lib.RefDatabase.ALL;
import com.google.common.collect.Lists;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.PageLinks;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.Capable;
import com.google.gerrit.common.data.ContributorAgreement;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.LabelTypes;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.data.PermissionRule.Action;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.GitReceivePackGroups;
import com.google.gerrit.server.config.GitUploadPackGroups;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.errors.IncorrectObjectTypeException;
import org.eclipse.jgit.lib.Ref;
import org.eclipse.jgit.lib.Repository;
import org.eclipse.jgit.revwalk.RevCommit;
import org.eclipse.jgit.revwalk.RevWalk;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
/** Access control management for a user accessing a project's data. */
public class ProjectControl {
public static final int VISIBLE = 1 << 0;
public static final int OWNER = 1 << 1;
private static final Logger log = LoggerFactory.getLogger(ProjectControl.class);
public static class GenericFactory {
private final ProjectCache projectCache;
@Inject
GenericFactory(final ProjectCache pc) {
projectCache = pc;
}
public ProjectControl controlFor(Project.NameKey nameKey, CurrentUser user)
throws NoSuchProjectException, IOException {
final ProjectState p = projectCache.checkedGet(nameKey);
if (p == null) {
throw new NoSuchProjectException(nameKey);
}
return p.controlFor(user);
}
public ProjectControl validateFor(Project.NameKey nameKey, int need,
CurrentUser user) throws NoSuchProjectException, IOException {
final ProjectControl c = controlFor(nameKey, user);
if ((need & VISIBLE) == VISIBLE && c.isVisible()) {
return c;
}
if ((need & OWNER) == OWNER && c.isOwner()) {
return c;
}
throw new NoSuchProjectException(nameKey);
}
}
public static class Factory {
private final Provider<PerRequestProjectControlCache> userCache;
@Inject
Factory(Provider<PerRequestProjectControlCache> uc) {
userCache = uc;
}
public ProjectControl controlFor(final Project.NameKey nameKey)
throws NoSuchProjectException {
return userCache.get().get(nameKey);
}
public ProjectControl validateFor(final Project.NameKey nameKey)
throws NoSuchProjectException {
return validateFor(nameKey, VISIBLE);
}
public ProjectControl ownerFor(final Project.NameKey nameKey)
throws NoSuchProjectException {
return validateFor(nameKey, OWNER);
}
public ProjectControl validateFor(final Project.NameKey nameKey,
final int need) throws NoSuchProjectException {
final ProjectControl c = controlFor(nameKey);
if ((need & VISIBLE) == VISIBLE && c.isVisible()) {
return c;
}
if ((need & OWNER) == OWNER && c.isOwner()) {
return c;
}
throw new NoSuchProjectException(nameKey);
}
}
interface AssistedFactory {
ProjectControl create(CurrentUser who, ProjectState ps);
}
private final Set<AccountGroup.UUID> uploadGroups;
private final Set<AccountGroup.UUID> receiveGroups;
private final String canonicalWebUrl;
private final CurrentUser user;
private final ProjectState state;
private final GitRepositoryManager repoManager;
private final PermissionCollection.Factory permissionFilter;
private final Collection<ContributorAgreement> contributorAgreements;
private List<SectionMatcher> allSections;
private List<SectionMatcher> localSections;
private LabelTypes labelTypes;
private Map<String, RefControl> refControls;
private Boolean declaredOwner;
@Inject
ProjectControl(@GitUploadPackGroups Set<AccountGroup.UUID> uploadGroups,
@GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups,
final ProjectCache pc, final PermissionCollection.Factory permissionFilter,
final GitRepositoryManager repoManager,
@CanonicalWebUrl @Nullable final String canonicalWebUrl,
@Assisted CurrentUser who, @Assisted ProjectState ps) {
this.repoManager = repoManager;
this.uploadGroups = uploadGroups;
this.receiveGroups = receiveGroups;
this.permissionFilter = permissionFilter;
this.contributorAgreements = pc.getAllProjects().getConfig().getContributorAgreements();
this.canonicalWebUrl = canonicalWebUrl;
user = who;
state = ps;
}
public ProjectControl forUser(CurrentUser who) {
ProjectControl r = state.controlFor(who);
// Not per-user, and reusing saves lookup time.
r.allSections = allSections;
return r;
}
public ChangeControl controlFor(final Change change) {
return new ChangeControl(controlForRef(change.getDest()), change);
}
public RefControl controlForRef(Branch.NameKey ref) {
return controlForRef(ref.get());
}
public RefControl controlForRef(String refName) {
if (refControls == null) {
refControls = new HashMap<String, RefControl>();
}
RefControl ctl = refControls.get(refName);
if (ctl == null) {
PermissionCollection relevant =
permissionFilter.filter(access(), refName, user.getUserName());
ctl = new RefControl(this, refName, relevant);
refControls.put(refName, ctl);
}
return ctl;
}
public CurrentUser getCurrentUser() {
return user;
}
public ProjectState getProjectState() {
return state;
}
public Project getProject() {
return state.getProject();
}
public LabelTypes getLabelTypes() {
if (labelTypes == null) {
labelTypes = state.getLabelTypes();
}
return labelTypes;
}
private boolean isHidden() {
return getProject().getState().equals(Project.State.HIDDEN);
}
/** Can this user see this project exists? */
public boolean isVisible() {
return (user instanceof InternalUser
|| canPerformOnAnyRef(Permission.READ)) && !isHidden();
}
public boolean canAddRefs() {
return (canPerformOnAnyRef(Permission.CREATE)
|| isOwnerAnyRef());
}
public boolean canUpload() {
for (SectionMatcher matcher : access()) {
AccessSection section = matcher.section;
if (section.getName().startsWith("refs/for/")) {
Permission permission = section.getPermission(Permission.PUSH);
if (permission != null
&& controlForRef(section.getName()).canPerform(Permission.PUSH)) {
return true;
}
}
}
return false;
}
/** Can this user see all the refs in this projects? */
public boolean allRefsAreVisible() {
return allRefsAreVisibleExcept(Collections.<String> emptySet());
}
public boolean allRefsAreVisibleExcept(Set<String> except) {
return user instanceof InternalUser
|| canPerformOnAllRefs(Permission.READ, except);
}
/** Is this user a project owner? Ownership does not imply {@link #isVisible()} */
public boolean isOwner() {
return isDeclaredOwner()
|| user.getCapabilities().canAdministrateServer();
}
private boolean isDeclaredOwner() {
if (declaredOwner == null) {
declaredOwner = state.isOwner(user.getEffectiveGroups());
}
return declaredOwner;
}
/** Does this user have ownership on at least one reference name? */
public boolean isOwnerAnyRef() {
return canPerformOnAnyRef(Permission.OWNER)
|| user.getCapabilities().canAdministrateServer();
}
/** @return true if the user can upload to at least one reference */
public Capable canPushToAtLeastOneRef() {
if (! canPerformOnAnyRef(Permission.PUSH) &&
! canPerformOnAnyRef(Permission.PUSH_TAG)) {
String pName = state.getProject().getName();
return new Capable("Upload denied for project '" + pName + "'");
}
if (state.isUseContributorAgreements()) {
return verifyActiveContributorAgreement();
}
return Capable.OK;
}
public Set<GroupReference> getAllGroups() {
return getGroups(access());
}
public Set<GroupReference> getLocalGroups() {
return getGroups(localAccess());
}
private static Set<GroupReference> getGroups(
final List<SectionMatcher> sectionMatcherList) {
final Set<GroupReference> all = new HashSet<GroupReference>();
for (final SectionMatcher matcher : sectionMatcherList) {
final AccessSection section = matcher.section;
for (final Permission permission : section.getPermissions()) {
for (final PermissionRule rule : permission.getRules()) {
all.add(rule.getGroup());
}
}
}
return all;
}
private Capable verifyActiveContributorAgreement() {
if (! (user.isIdentifiedUser())) {
return new Capable("Must be logged in to verify Contributor Agreement");
}
final IdentifiedUser iUser = (IdentifiedUser) user;
boolean hasContactInfo = !missing(iUser.getAccount().getFullName())
&& !missing(iUser.getAccount().getPreferredEmail())
&& iUser.getAccount().isContactFiled();
List<AccountGroup.UUID> okGroupIds = Lists.newArrayList();
List<AccountGroup.UUID> missingInfoGroupIds = Lists.newArrayList();
for (ContributorAgreement ca : contributorAgreements) {
List<AccountGroup.UUID> groupIds;
if (hasContactInfo || !ca.isRequireContactInformation()) {
groupIds = okGroupIds;
} else {
groupIds = missingInfoGroupIds;
}
for (PermissionRule rule : ca.getAccepted()) {
if ((rule.getAction() == Action.ALLOW) && (rule.getGroup() != null)
&& (rule.getGroup().getUUID() != null)) {
groupIds.add(new AccountGroup.UUID(rule.getGroup().getUUID().get()));
}
}
}
if (iUser.getEffectiveGroups().containsAnyOf(okGroupIds)) {
return Capable.OK;
}
if (iUser.getEffectiveGroups().containsAnyOf(missingInfoGroupIds)) {
final StringBuilder msg = new StringBuilder();
for (ContributorAgreement ca : contributorAgreements) {
if (ca.isRequireContactInformation()) {
msg.append(ca.getName());
break;
}
}
msg.append(" contributor agreement requires");
msg.append(" current contact information.\n");
if (canonicalWebUrl != null) {
msg.append("\nPlease review your contact information");
msg.append(":\n\n ");
msg.append(canonicalWebUrl);
msg.append("#");
msg.append(PageLinks.SETTINGS_CONTACT);
msg.append("\n");
}
msg.append("\n");
return new Capable(msg.toString());
}
final StringBuilder msg = new StringBuilder();
msg.append(" A Contributor Agreement must be completed before uploading");
if (canonicalWebUrl != null) {
msg.append(":\n\n ");
msg.append(canonicalWebUrl);
msg.append("#");
msg.append(PageLinks.SETTINGS_AGREEMENTS);
msg.append("\n");
} else {
msg.append(".");
}
msg.append("\n");
return new Capable(msg.toString());
}
private static boolean missing(final String value) {
return value == null || value.trim().equals("");
}
private boolean canPerformOnAnyRef(String permissionName) {
for (SectionMatcher matcher : access()) {
AccessSection section = matcher.section;
Permission permission = section.getPermission(permissionName);
if (permission == null) {
continue;
}
for (PermissionRule rule : permission.getRules()) {
if (rule.isBlock() || rule.isDeny() || !match(rule)) {
continue;
}
// Being in a group that was granted this permission is only an
// approximation. There might be overrides and doNotInherit
// that would render this to be false.
//
if (controlForRef(section.getName()).canPerform(permissionName)) {
return true;
} else {
break;
}
}
}
return false;
}
private boolean canPerformOnAllRefs(String permission, Set<String> except) {
boolean canPerform = false;
Set<String> patterns = allRefPatterns(permission);
if (patterns.contains(AccessSection.ALL)) {
// Only possible if granted on the pattern that
// matches every possible reference. Check all
// patterns also have the permission.
//
for (final String pattern : patterns) {
if (controlForRef(pattern).canPerform(permission)) {
canPerform = true;
} else if (except.contains(pattern)) {
continue;
} else {
return false;
}
}
}
return canPerform;
}
private Set<String> allRefPatterns(String permissionName) {
Set<String> all = new HashSet<String>();
for (SectionMatcher matcher : access()) {
AccessSection section = matcher.section;
Permission permission = section.getPermission(permissionName);
if (permission != null) {
all.add(section.getName());
}
}
return all;
}
private List<SectionMatcher> access() {
if (allSections == null) {
allSections = state.getAllSections();
}
return allSections;
}
private List<SectionMatcher> localAccess() {
if (localSections == null) {
localSections = state.getLocalAccessSections();
}
return localSections;
}
boolean match(PermissionRule rule) {
return match(rule.getGroup().getUUID());
}
boolean match(AccountGroup.UUID uuid) {
if (AccountGroup.PROJECT_OWNERS.equals(uuid)) {
return isDeclaredOwner();
} else {
return user.getEffectiveGroups().contains(uuid);
}
}
public boolean canRunUploadPack() {
for (AccountGroup.UUID group : uploadGroups) {
if (match(group)) {
return true;
}
}
return false;
}
public boolean canRunReceivePack() {
for (AccountGroup.UUID group : receiveGroups) {
if (match(group)) {
return true;
}
}
return false;
}
public boolean canReadCommit(RevWalk rw, RevCommit commit) {
if (controlForRef("refs/*").canPerform(Permission.READ)) {
return true;
}
Project.NameKey projName = state.getProject().getNameKey();
try {
Repository repo = repoManager.openRepository(projName);
try {
Map<String, Ref> allRefs = repo.getRefDatabase().getRefs(ALL);
for (Entry<String, Ref> entry : allRefs.entrySet()) {
String refName = entry.getKey();
if (!refName.startsWith("refs/heads") && !refName.startsWith("refs/tags")) {
continue;
}
RevCommit tip;
try {
tip = rw.parseCommit(entry.getValue().getObjectId());
} catch (IncorrectObjectTypeException e) {
continue;
}
if (rw.isMergedInto(commit, tip)
&& controlForRef(entry.getKey()).canPerform(Permission.READ)) {
return true;
}
}
} finally {
repo.close();
}
} catch (IOException e) {
String msg =
String.format(
"Cannot verify permissions to commit object %s in repository %s",
commit.name(), projName.get());
log.error(msg, e);
}
return false;
}
}