Merge "Allow to propose changes to access rights through code review"

This commit is contained in:
Shawn Pearce
2012-06-19 17:32:48 -07:00
committed by gerrit code review
26 changed files with 492 additions and 199 deletions

View File

@@ -15,41 +15,26 @@
package com.google.gerrit.httpd.rpc.project;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.data.ProjectAccess;
import com.google.gerrit.common.errors.InvalidNameException;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupBackends;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.RefControl;
import com.google.gwtorm.server.OrmConcurrencyException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import javax.annotation.Nullable;
class ChangeProjectAccess extends Handler<ProjectAccess> {
class ChangeProjectAccess extends ProjectAccessHandler<ProjectAccess> {
interface Factory {
ChangeProjectAccess create(@Assisted Project.NameKey projectName,
@Nullable @Assisted ObjectId base,
@@ -58,15 +43,7 @@ class ChangeProjectAccess extends Handler<ProjectAccess> {
}
private final ProjectAccessFactory.Factory projectAccessFactory;
private final ProjectControl.Factory projectControlFactory;
private final ProjectCache projectCache;
private final GroupBackend groupBackend;
private final MetaDataUpdate.User metaDataUpdateFactory;
private final Project.NameKey projectName;
private final ObjectId base;
private List<AccessSection> sectionList;
private String message;
@Inject
ChangeProjectAccess(final ProjectAccessFactory.Factory projectAccessFactory,
@@ -78,132 +55,17 @@ class ChangeProjectAccess extends Handler<ProjectAccess> {
@Nullable @Assisted final ObjectId base,
@Assisted List<AccessSection> sectionList,
@Nullable @Assisted String message) {
super(projectControlFactory, groupBackend, metaDataUpdateFactory,
projectName, base, sectionList, message, true);
this.projectAccessFactory = projectAccessFactory;
this.projectControlFactory = projectControlFactory;
this.projectCache = projectCache;
this.groupBackend = groupBackend;
this.metaDataUpdateFactory = metaDataUpdateFactory;
this.projectName = projectName;
this.base = base;
this.sectionList = sectionList;
this.message = message;
}
@Override
public ProjectAccess call() throws NoSuchProjectException, IOException,
ConfigInvalidException, InvalidNameException, NoSuchGroupException,
OrmConcurrencyException {
final ProjectControl projectControl =
projectControlFactory.controlFor(projectName);
final MetaDataUpdate md;
try {
md = metaDataUpdateFactory.create(projectName);
} catch (RepositoryNotFoundException notFound) {
throw new NoSuchProjectException(projectName);
}
try {
ProjectConfig config = ProjectConfig.read(md, base);
Set<String> toDelete = scanSectionNames(config);
for (AccessSection section : mergeSections(sectionList)) {
String name = section.getName();
if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
if (!projectControl.isOwner()) {
continue;
}
replace(config, toDelete, section);
} else if (AccessSection.isValid(name)) {
if (!projectControl.controlForRef(name).isOwner()) {
continue;
}
RefControl.validateRefPattern(name);
replace(config, toDelete, section);
}
}
for (String name : toDelete) {
if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
if (projectControl.isOwner()) {
config.remove(config.getAccessSection(name));
}
} else if (projectControl.controlForRef(name).isOwner()) {
config.remove(config.getAccessSection(name));
}
}
if (message != null && !message.isEmpty()) {
if (!message.endsWith("\n")) {
message += "\n";
}
md.setMessage(message);
} else {
md.setMessage("Modify access rules\n");
}
if (config.commit(md)) {
projectCache.evict(config.getProject());
return projectAccessFactory.create(projectName).call();
} else {
throw new OrmConcurrencyException("Cannot update " + projectName);
}
} finally {
md.close();
}
}
private void replace(ProjectConfig config, Set<String> toDelete,
AccessSection section) throws NoSuchGroupException {
for (Permission permission : section.getPermissions()) {
for (PermissionRule rule : permission.getRules()) {
lookupGroup(rule);
}
}
config.replace(section);
toDelete.remove(section.getName());
}
private static List<AccessSection> mergeSections(List<AccessSection> src) {
Map<String, AccessSection> map = new LinkedHashMap<String, AccessSection>();
for (AccessSection section : src) {
if (section.getPermissions().isEmpty()) {
continue;
}
AccessSection prior = map.get(section.getName());
if (prior != null) {
prior.mergeFrom(section);
} else {
map.put(section.getName(), section);
}
}
return new ArrayList<AccessSection>(map.values());
}
private static Set<String> scanSectionNames(ProjectConfig config) {
Set<String> names = new HashSet<String>();
for (AccessSection section : config.getAccessSections()) {
names.add(section.getName());
}
return names;
}
private void lookupGroup(PermissionRule rule) throws NoSuchGroupException {
GroupReference ref = rule.getGroup();
if (ref.getUUID() == null) {
final GroupReference group =
GroupBackends.findBestSuggestion(groupBackend, ref.getName());
if (group == null) {
throw new NoSuchGroupException(ref.getName());
}
ref.setUUID(group.getUUID());
}
protected ProjectAccess updateProjectConfig(ProjectConfig config,
MetaDataUpdate md) throws IOException, NoSuchProjectException, ConfigInvalidException {
config.commit(md);
projectCache.evict(config.getProject());
return projectAccessFactory.create(projectName).call();
}
}

View File

@@ -86,10 +86,11 @@ class ChangeProjectSettings extends Handler<ProjectDetail> {
config.getProject().copySettingsFrom(update);
md.setMessage("Modified project settings\n");
if (config.commit(md)) {
try {
config.commit(md);
mgr.setProjectDescription(projectName, update.getDescription());
userCache.get().evict(config.getProject());
} else {
} catch (IOException e) {
throw new OrmConcurrencyException("Cannot update " + projectName);
}
} catch (ConfigInvalidException err) {

View File

@@ -96,10 +96,9 @@ class ProjectAccessFactory extends Handler<ProjectAccess> {
if (config.updateGroupNames(groupBackend)) {
md.setMessage("Update group names\n");
if (config.commit(md)) {
projectCache.evict(config.getProject());
pc = open();
}
config.commit(md);
projectCache.evict(config.getProject());
pc = open();
} else if (config.getRevision() != null
&& !config.getRevision().equals(
pc.getProjectState().getConfig().getRevision())) {
@@ -196,6 +195,8 @@ class ProjectAccessFactory extends Handler<ProjectAccess> {
detail.setLocal(local);
detail.setOwnerOf(ownerOf);
detail.setCanUpload(pc.isOwner()
|| (metaConfigControl.isVisible() && metaConfigControl.canUpload()));
detail.setConfigVisible(pc.isOwner() || metaConfigControl.isVisible());
return detail;
}

View File

@@ -0,0 +1,190 @@
// Copyright (C) 2012 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.httpd.rpc.project;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.common.errors.InvalidNameException;
import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.httpd.rpc.Handler;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupBackends;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gerrit.server.project.RefControl;
import com.google.gwtorm.server.OrmException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.errors.RepositoryNotFoundException;
import org.eclipse.jgit.lib.ObjectId;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
public abstract class ProjectAccessHandler<T> extends Handler<T> {
private final ProjectControl.Factory projectControlFactory;
protected final GroupBackend groupBackend;
private final MetaDataUpdate.User metaDataUpdateFactory;
protected final Project.NameKey projectName;
protected final ObjectId base;
private List<AccessSection> sectionList;
protected String message;
private boolean checkIfOwner;
protected ProjectAccessHandler(
final ProjectControl.Factory projectControlFactory,
final GroupBackend groupBackend,
final MetaDataUpdate.User metaDataUpdateFactory,
final Project.NameKey projectName, final ObjectId base,
final List<AccessSection> sectionList, final String message,
final boolean checkIfOwner) {
this.projectControlFactory = projectControlFactory;
this.groupBackend = groupBackend;
this.metaDataUpdateFactory = metaDataUpdateFactory;
this.projectName = projectName;
this.base = base;
this.sectionList = sectionList;
this.message = message;
this.checkIfOwner = checkIfOwner;
}
@Override
public final T call() throws NoSuchProjectException, IOException,
ConfigInvalidException, InvalidNameException, NoSuchGroupException,
OrmException {
final ProjectControl projectControl =
projectControlFactory.controlFor(projectName);
final MetaDataUpdate md;
try {
md = metaDataUpdateFactory.create(projectName);
} catch (RepositoryNotFoundException notFound) {
throw new NoSuchProjectException(projectName);
}
try {
ProjectConfig config = ProjectConfig.read(md, base);
Set<String> toDelete = scanSectionNames(config);
for (AccessSection section : mergeSections(sectionList)) {
String name = section.getName();
if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
if (checkIfOwner && !projectControl.isOwner()) {
continue;
}
replace(config, toDelete, section);
} else if (AccessSection.isValid(name)) {
if (checkIfOwner && !projectControl.controlForRef(name).isOwner()) {
continue;
}
RefControl.validateRefPattern(name);
replace(config, toDelete, section);
}
}
for (String name : toDelete) {
if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
if (!checkIfOwner || projectControl.isOwner()) {
config.remove(config.getAccessSection(name));
}
} else if (!checkIfOwner || projectControl.controlForRef(name).isOwner()) {
config.remove(config.getAccessSection(name));
}
}
if (message != null && !message.isEmpty()) {
if (!message.endsWith("\n")) {
message += "\n";
}
md.setMessage(message);
} else {
md.setMessage("Modify access rules\n");
}
return updateProjectConfig(config, md);
} finally {
md.close();
}
}
protected abstract T updateProjectConfig(ProjectConfig config,
MetaDataUpdate md) throws IOException, NoSuchProjectException,
ConfigInvalidException, OrmException;
private void replace(ProjectConfig config, Set<String> toDelete,
AccessSection section) throws NoSuchGroupException {
for (Permission permission : section.getPermissions()) {
for (PermissionRule rule : permission.getRules()) {
lookupGroup(rule);
}
}
config.replace(section);
toDelete.remove(section.getName());
}
private static List<AccessSection> mergeSections(List<AccessSection> src) {
Map<String, AccessSection> map = new LinkedHashMap<String, AccessSection>();
for (AccessSection section : src) {
if (section.getPermissions().isEmpty()) {
continue;
}
AccessSection prior = map.get(section.getName());
if (prior != null) {
prior.mergeFrom(section);
} else {
map.put(section.getName(), section);
}
}
return new ArrayList<AccessSection>(map.values());
}
private static Set<String> scanSectionNames(ProjectConfig config) {
Set<String> names = new HashSet<String>();
for (AccessSection section : config.getAccessSections()) {
names.add(section.getName());
}
return names;
}
private void lookupGroup(PermissionRule rule) throws NoSuchGroupException {
GroupReference ref = rule.getGroup();
if (ref.getUUID() == null) {
final GroupReference group =
GroupBackends.findBestSuggestion(groupBackend, ref.getName());
if (group == null) {
throw new NoSuchGroupException(ref.getName());
}
ref.setUUID(group.getUUID());
}
}
}

View File

@@ -20,6 +20,7 @@ import com.google.gerrit.common.data.ProjectAccess;
import com.google.gerrit.common.data.ProjectAdminService;
import com.google.gerrit.common.data.ProjectDetail;
import com.google.gerrit.reviewdb.client.Branch;
import com.google.gerrit.reviewdb.client.Change;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gwtjsonrpc.common.AsyncCallback;
import com.google.gwtjsonrpc.common.VoidResult;
@@ -33,6 +34,7 @@ import java.util.Set;
class ProjectAdminServiceImpl implements ProjectAdminService {
private final AddBranch.Factory addBranchFactory;
private final ChangeProjectAccess.Factory changeProjectAccessFactory;
private final ReviewProjectAccess.Factory reviewProjectAccessFactory;
private final ChangeProjectSettings.Factory changeProjectSettingsFactory;
private final DeleteBranches.Factory deleteBranchesFactory;
private final ListBranches.Factory listBranchesFactory;
@@ -44,6 +46,7 @@ class ProjectAdminServiceImpl implements ProjectAdminService {
@Inject
ProjectAdminServiceImpl(final AddBranch.Factory addBranchFactory,
final ChangeProjectAccess.Factory changeProjectAccessFactory,
final ReviewProjectAccess.Factory reviewProjectAccessFactory,
final ChangeProjectSettings.Factory changeProjectSettingsFactory,
final DeleteBranches.Factory deleteBranchesFactory,
final ListBranches.Factory listBranchesFactory,
@@ -53,6 +56,7 @@ class ProjectAdminServiceImpl implements ProjectAdminService {
final CreateProjectHandler.Factory createNewProjectFactory) {
this.addBranchFactory = addBranchFactory;
this.changeProjectAccessFactory = changeProjectAccessFactory;
this.reviewProjectAccessFactory = reviewProjectAccessFactory;
this.changeProjectSettingsFactory = changeProjectSettingsFactory;
this.deleteBranchesFactory = deleteBranchesFactory;
this.listBranchesFactory = listBranchesFactory;
@@ -98,6 +102,14 @@ class ProjectAdminServiceImpl implements ProjectAdminService {
changeProjectAccessFactory.create(projectName, base, sections, msg).to(cb);
}
@Override
public void reviewProjectAccess(Project.NameKey projectName,
String baseRevision, String msg, List<AccessSection> sections,
AsyncCallback<Change.Id> cb) {
ObjectId base = ObjectId.fromString(baseRevision);
reviewProjectAccessFactory.create(projectName, base, sections, msg).to(cb);
}
@Override
public void listBranches(final Project.NameKey projectName,
final AsyncCallback<ListBranchesResult> callback) {

View File

@@ -30,6 +30,7 @@ public class ProjectModule extends RpcServletModule {
protected void configure() {
factory(AddBranch.Factory.class);
factory(ChangeProjectAccess.Factory.class);
factory(ReviewProjectAccess.Factory.class);
factory(CreateProjectHandler.Factory.class);
factory(ChangeProjectSettings.Factory.class);
factory(DeleteBranches.Factory.class);

View File

@@ -0,0 +1,127 @@
// Copyright (C) 2012 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.httpd.rpc.project;
import com.google.gerrit.common.data.AccessSection;
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.PatchSet;
import com.google.gerrit.reviewdb.client.PatchSetInfo;
import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RevId;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.ChangeUtil;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.patch.AddReviewer;
import com.google.gerrit.server.patch.PatchSetInfoFactory;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectControl;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.revwalk.RevCommit;
import java.io.IOException;
import java.util.Collections;
import java.util.List;
import javax.annotation.Nullable;
public class ReviewProjectAccess extends ProjectAccessHandler<Change.Id> {
interface Factory {
ReviewProjectAccess create(@Assisted Project.NameKey projectName,
@Nullable @Assisted ObjectId base,
@Assisted List<AccessSection> sectionList,
@Nullable @Assisted String message);
}
private final ReviewDb db;
private final IdentifiedUser user;
private final PatchSetInfoFactory patchSetInfoFactory;
private final AddReviewer.Factory addReviewerFactory;
@Inject
ReviewProjectAccess(final ProjectControl.Factory projectControlFactory,
final GroupBackend groupBackend,
final MetaDataUpdate.User metaDataUpdateFactory, final ReviewDb db,
final IdentifiedUser user, final PatchSetInfoFactory patchSetInfoFactory,
final AddReviewer.Factory addReviewerFactory,
@Assisted final Project.NameKey projectName,
@Nullable @Assisted final ObjectId base,
@Assisted List<AccessSection> sectionList,
@Nullable @Assisted String message) {
super(projectControlFactory, groupBackend, metaDataUpdateFactory,
projectName, base, sectionList, message, false);
this.db = db;
this.user = user;
this.patchSetInfoFactory = patchSetInfoFactory;
this.addReviewerFactory = addReviewerFactory;
}
@Override
protected Change.Id updateProjectConfig(ProjectConfig config, MetaDataUpdate md)
throws IOException, NoSuchProjectException, ConfigInvalidException, OrmException {
int nextChangeId = db.nextChangeId();
PatchSet.Id patchSetId = new PatchSet.Id(new Change.Id(nextChangeId), 1);
final PatchSet ps = new PatchSet(patchSetId);
RevCommit commit = config.commitToNewRef(md, ps.getRefName());
if (commit.getId().equals(base)) {
return null;
}
Change.Key changeKey = new Change.Key("I" + commit.name());
final Change change =
new Change(changeKey, new Change.Id(nextChangeId), user.getAccountId(),
new Branch.NameKey(config.getProject().getNameKey(),
GitRepositoryManager.REF_CONFIG));
change.nextPatchSetId();
ps.setCreatedOn(change.getCreatedOn());
ps.setUploader(user.getAccountId());
ps.setRevision(new RevId(commit.name()));
db.patchSets().insert(Collections.singleton(ps));
final PatchSetInfo info = patchSetInfoFactory.get(commit, ps.getId());
change.setCurrentPatchSet(info);
ChangeUtil.updated(change);
db.changes().insert(Collections.singleton(change));
addProjectOwnersAsReviewers(change.getId());
return change.getId();
}
private void addProjectOwnersAsReviewers(final Change.Id changeId) {
final String projectOwners =
groupBackend.get(AccountGroup.PROJECT_OWNERS).getName();
try {
addReviewerFactory.create(changeId, Collections.singleton(projectOwners),
false).call();
} catch (Exception e) {
// one of the owner groups is not visible to the user and this it why it
// can't be added as reviewer
}
}
}