Enforce that All-Users must inherit from All-Projects

For groups in NoteDb, we manage group ownership through permissions on
the group ref. During the migration, we prevent the mutation of
Gerrit-managed permissions for group refs to ensure the ReviewDb and
NoteDb data does not get out of sync.

These enforcements are in place for All-Users and All-Projects. To
prevent users from changing these properties by inheritance, we enforce
a fixed inheritance of All-Users <= All-Projects.

In addition, this commit adds a schema migration to migrate wrong
inheritance.

Change-Id: Id8e4f957d316cf401463dcce2042e604e19037bd
This commit is contained in:
Patrick Hiesel
2017-11-07 13:24:20 +01:00
parent b4a4c58120
commit 14959d1542
10 changed files with 258 additions and 13 deletions

View File

@@ -27,6 +27,7 @@ import com.google.gerrit.common.errors.NoSuchGroupException;
import com.google.gerrit.common.errors.PermissionDeniedException; import com.google.gerrit.common.errors.PermissionDeniedException;
import com.google.gerrit.common.errors.UpdateParentFailedException; import com.google.gerrit.common.errors.UpdateParentFailedException;
import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.UnprocessableEntityException; import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.httpd.rpc.Handler; import com.google.gerrit.httpd.rpc.Handler;
@@ -189,7 +190,7 @@ public abstract class ProjectAccessHandler<T> extends Handler<T> {
+ "not an administrator. You may save the modifications for review " + "not an administrator. You may save the modifications for review "
+ "so that an administrator can approve them.", + "so that an administrator can approve them.",
e); e);
} catch (ResourceConflictException | UnprocessableEntityException e) { } catch (ResourceConflictException | UnprocessableEntityException | BadRequestException e) {
throw new UpdateParentFailedException(e.getMessage(), e); throw new UpdateParentFailedException(e.getMessage(), e);
} }
config.getProject().setParentName(parentProjectName); config.getProject().setParentName(parentProjectName);

View File

@@ -427,6 +427,14 @@ public class CommitValidators {
throw new ConfigInvalidException("invalid project configuration"); throw new ConfigInvalidException("invalid project configuration");
} }
} }
if (allUsers.equals(receiveEvent.project.getNameKey())
&& !allProjects.equals(cfg.getProject().getParent(allProjects))) {
addError("Invalid project configuration:", messages);
addError(
String.format(" %s must inherit from %s", allUsers.get(), allProjects.get()),
messages);
throw new ConfigInvalidException("invalid project configuration");
}
} catch (ConfigInvalidException | IOException e) { } catch (ConfigInvalidException | IOException e) {
log.error( log.error(
"User " "User "

View File

@@ -117,6 +117,7 @@ public class MergeValidators {
+ "The change must be submitted by a Gerrit administrator."; + "The change must be submitted by a Gerrit administrator.";
private final AllProjectsName allProjectsName; private final AllProjectsName allProjectsName;
private final AllUsersName allUsersName;
private final ProjectCache projectCache; private final ProjectCache projectCache;
private final PermissionBackend permissionBackend; private final PermissionBackend permissionBackend;
private final DynamicMap<ProjectConfigEntry> pluginConfigEntries; private final DynamicMap<ProjectConfigEntry> pluginConfigEntries;
@@ -128,10 +129,12 @@ public class MergeValidators {
@Inject @Inject
public ProjectConfigValidator( public ProjectConfigValidator(
AllProjectsName allProjectsName, AllProjectsName allProjectsName,
AllUsersName allUsersName,
ProjectCache projectCache, ProjectCache projectCache,
PermissionBackend permissionBackend, PermissionBackend permissionBackend,
DynamicMap<ProjectConfigEntry> pluginConfigEntries) { DynamicMap<ProjectConfigEntry> pluginConfigEntries) {
this.allProjectsName = allProjectsName; this.allProjectsName = allProjectsName;
this.allUsersName = allUsersName;
this.projectCache = projectCache; this.projectCache = projectCache;
this.permissionBackend = permissionBackend; this.permissionBackend = permissionBackend;
this.pluginConfigEntries = pluginConfigEntries; this.pluginConfigEntries = pluginConfigEntries;
@@ -168,7 +171,12 @@ public class MergeValidators {
log.warn("Cannot check ADMINISTRATE_SERVER", e); log.warn("Cannot check ADMINISTRATE_SERVER", e);
throw new MergeValidationException("validation unavailable"); throw new MergeValidationException("validation unavailable");
} }
if (allUsersName.equals(destProject.getNameKey())
&& !allProjectsName.equals(newParent)) {
throw new MergeValidationException(
String.format(
" %s must inherit from %s", allUsersName.get(), allProjectsName.get()));
}
if (projectCache.get(newParent) == null) { if (projectCache.get(newParent) == null) {
throw new MergeValidationException(PARENT_NOT_FOUND); throw new MergeValidationException(PARENT_NOT_FOUND);
} }

View File

@@ -212,13 +212,14 @@ public class SetAccessUtil {
} }
} }
void setParentName( public void setParentName(
IdentifiedUser identifiedUser, IdentifiedUser identifiedUser,
ProjectConfig config, ProjectConfig config,
Project.NameKey projectName, Project.NameKey projectName,
Project.NameKey newParentProjectName, Project.NameKey newParentProjectName,
boolean checkAdmin) boolean checkAdmin)
throws ResourceConflictException, AuthException, PermissionBackendException { throws ResourceConflictException, AuthException, PermissionBackendException,
BadRequestException {
if (newParentProjectName != null if (newParentProjectName != null
&& !config.getProject().getNameKey().equals(allProjects) && !config.getProject().getNameKey().equals(allProjects)
&& !config.getProject().getParent(allProjects).equals(newParentProjectName)) { && !config.getProject().getParent(allProjects).equals(newParentProjectName)) {

View File

@@ -21,6 +21,7 @@ import com.google.common.base.Strings;
import com.google.common.collect.Iterables; import com.google.common.collect.Iterables;
import com.google.gerrit.extensions.api.projects.ParentInput; import com.google.gerrit.extensions.api.projects.ParentInput;
import com.google.gerrit.extensions.restapi.AuthException; import com.google.gerrit.extensions.restapi.AuthException;
import com.google.gerrit.extensions.restapi.BadRequestException;
import com.google.gerrit.extensions.restapi.ResourceConflictException; import com.google.gerrit.extensions.restapi.ResourceConflictException;
import com.google.gerrit.extensions.restapi.ResourceNotFoundException; import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.extensions.restapi.RestModifyView; import com.google.gerrit.extensions.restapi.RestModifyView;
@@ -28,6 +29,7 @@ import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.server.IdentifiedUser; import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.config.AllProjectsName; import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.MetaDataUpdate; import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig; import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.permissions.GlobalPermission; import com.google.gerrit.server.permissions.GlobalPermission;
@@ -45,29 +47,34 @@ public class SetParent implements RestModifyView<ProjectResource, ParentInput> {
private final PermissionBackend permissionBackend; private final PermissionBackend permissionBackend;
private final MetaDataUpdate.Server updateFactory; private final MetaDataUpdate.Server updateFactory;
private final AllProjectsName allProjects; private final AllProjectsName allProjects;
private final AllUsersName allUsers;
@Inject @Inject
SetParent( SetParent(
ProjectCache cache, ProjectCache cache,
PermissionBackend permissionBackend, PermissionBackend permissionBackend,
MetaDataUpdate.Server updateFactory, MetaDataUpdate.Server updateFactory,
AllProjectsName allProjects) { AllProjectsName allProjects,
AllUsersName allUsers) {
this.cache = cache; this.cache = cache;
this.permissionBackend = permissionBackend; this.permissionBackend = permissionBackend;
this.updateFactory = updateFactory; this.updateFactory = updateFactory;
this.allProjects = allProjects; this.allProjects = allProjects;
this.allUsers = allUsers;
} }
@Override @Override
public String apply(ProjectResource rsrc, ParentInput input) public String apply(ProjectResource rsrc, ParentInput input)
throws AuthException, ResourceConflictException, ResourceNotFoundException, throws AuthException, ResourceConflictException, ResourceNotFoundException,
UnprocessableEntityException, IOException, PermissionBackendException { UnprocessableEntityException, IOException, PermissionBackendException,
BadRequestException {
return apply(rsrc, input, true); return apply(rsrc, input, true);
} }
public String apply(ProjectResource rsrc, ParentInput input, boolean checkIfAdmin) public String apply(ProjectResource rsrc, ParentInput input, boolean checkIfAdmin)
throws AuthException, ResourceConflictException, ResourceNotFoundException, throws AuthException, ResourceConflictException, ResourceNotFoundException,
UnprocessableEntityException, IOException, PermissionBackendException { UnprocessableEntityException, IOException, PermissionBackendException,
BadRequestException {
IdentifiedUser user = rsrc.getUser().asIdentifiedUser(); IdentifiedUser user = rsrc.getUser().asIdentifiedUser();
String parentName = String parentName =
MoreObjects.firstNonNull(Strings.emptyToNull(input.parent), allProjects.get()); MoreObjects.firstNonNull(Strings.emptyToNull(input.parent), allProjects.get());
@@ -102,11 +109,16 @@ public class SetParent implements RestModifyView<ProjectResource, ParentInput> {
public void validateParentUpdate( public void validateParentUpdate(
Project.NameKey project, IdentifiedUser user, String newParent, boolean checkIfAdmin) Project.NameKey project, IdentifiedUser user, String newParent, boolean checkIfAdmin)
throws AuthException, ResourceConflictException, UnprocessableEntityException, throws AuthException, ResourceConflictException, UnprocessableEntityException,
PermissionBackendException { PermissionBackendException, BadRequestException {
if (checkIfAdmin) { if (checkIfAdmin) {
permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER); permissionBackend.user(user).check(GlobalPermission.ADMINISTRATE_SERVER);
} }
if (project.equals(allUsers) && !allProjects.equals(newParent)) {
throw new BadRequestException(
String.format("%s must inherit from %s", allUsers.get(), allProjects.get()));
}
if (project.equals(allProjects)) { if (project.equals(allProjects)) {
throw new ResourceConflictException("cannot set parent of " + allProjects.get()); throw new ResourceConflictException("cannot set parent of " + allProjects.get());
} }

View File

@@ -35,7 +35,7 @@ import java.util.concurrent.TimeUnit;
/** A version of the database schema. */ /** A version of the database schema. */
public abstract class SchemaVersion { public abstract class SchemaVersion {
/** The current schema version. */ /** The current schema version. */
public static final Class<Schema_161> C = Schema_161.class; public static final Class<Schema_162> C = Schema_162.class;
public static int getBinaryVersion() { public static int getBinaryVersion() {
return guessVersion(C); return guessVersion(C);

View File

@@ -0,0 +1,71 @@
// Copyright (C) 2017 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.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
import com.google.gerrit.server.git.GitRepositoryManager;
import com.google.gerrit.server.git.MetaDataUpdate;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import com.google.inject.Provider;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
public class Schema_162 extends SchemaVersion {
private final GitRepositoryManager repoManager;
private final AllProjectsName allProjectsName;
private final AllUsersName allUsersName;
private final PersonIdent serverUser;
@Inject
Schema_162(
Provider<Schema_161> prior,
GitRepositoryManager repoManager,
AllProjectsName allProjectsName,
AllUsersName allUsersName,
@GerritPersonIdent PersonIdent serverUser) {
super(prior);
this.repoManager = repoManager;
this.allProjectsName = allProjectsName;
this.allUsersName = allUsersName;
this.serverUser = serverUser;
}
@Override
protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
try (Repository git = repoManager.openRepository(allUsersName);
MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, git)) {
ProjectConfig cfg = ProjectConfig.read(md);
if (allProjectsName.equals(cfg.getProject().getParent(allProjectsName))) {
return;
}
cfg.getProject().setParentName(allProjectsName);
md.getCommitBuilder().setAuthor(serverUser);
md.getCommitBuilder().setCommitter(serverUser);
md.setMessage(
String.format("Make %s inherit from %s", allUsersName.get(), allProjectsName.get()));
cfg.commit(md);
} catch (ConfigInvalidException | IOException ex) {
throw new OrmException(ex);
}
}
}

View File

@@ -910,6 +910,30 @@ public class GroupsIT extends AbstractDaemonTest {
r.assertOkStatus(); r.assertOkStatus();
} }
@Test
public void pushCustomInheritanceForAllUsersFails() throws Exception {
TestRepository<InMemoryRepository> repo = cloneProject(allUsers, RefNames.REFS_CONFIG);
String config =
gApi.projects()
.name(allUsers.get())
.branch(RefNames.REFS_CONFIG)
.file("project.config")
.asString();
Config cfg = new Config();
cfg.fromText(config);
cfg.setString("access", null, "inheritFrom", project.get());
config = cfg.toText();
PushOneCommit.Result r =
pushFactory
.create(db, admin.getIdent(), repo, "Subject", "project.config", config)
.to(RefNames.REFS_CONFIG);
r.assertErrorStatus("invalid project configuration");
r.assertMessage("All-Users must inherit from All-Projects");
}
private void pushToGroupBranchForReviewAndSubmit(Project.NameKey project, String expectedError) private void pushToGroupBranchForReviewAndSubmit(Project.NameKey project, String expectedError)
throws Exception { throws Exception {
grantLabel( grantLabel(

View File

@@ -39,9 +39,13 @@ import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
import com.google.gerrit.reviewdb.client.AccountGroup; import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.reviewdb.client.Project; import com.google.gerrit.reviewdb.client.Project;
import com.google.gerrit.reviewdb.client.RefNames; import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllProjectsNameProvider; import com.google.gerrit.server.config.AllProjectsNameProvider;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.git.ProjectConfig;
import com.google.gerrit.server.group.InternalGroup; import com.google.gerrit.server.group.InternalGroup;
import com.google.gerrit.server.group.SystemGroupBackend; import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.inject.Inject;
import java.util.HashMap; import java.util.HashMap;
import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository; import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
import org.eclipse.jgit.junit.TestRepository; import org.eclipse.jgit.junit.TestRepository;
@@ -60,6 +64,9 @@ public class AccessIT extends AbstractDaemonTest {
private static final String LABEL_CODE_REVIEW = "Code-Review"; private static final String LABEL_CODE_REVIEW = "Code-Review";
@Inject private AllProjectsName allProjects;
@Inject private AllUsersName allUsers;
private String newProjectName; private String newProjectName;
private ProjectApi pApi; private ProjectApi pApi;
@@ -480,7 +487,7 @@ public class AccessIT extends AbstractDaemonTest {
gApi.projects() gApi.projects()
.name(allProjects.get()) .name(allProjects.get())
.branch(RefNames.REFS_CONFIG) .branch(RefNames.REFS_CONFIG)
.file("project.config") .file(ProjectConfig.PROJECT_CONFIG)
.asString(); .asString();
// Append and push unknown permission // Append and push unknown permission
@@ -490,7 +497,7 @@ public class AccessIT extends AbstractDaemonTest {
config = cfg.toText(); config = cfg.toText();
PushOneCommit push = PushOneCommit push =
pushFactory.create( pushFactory.create(
db, admin.getIdent(), allProjectsRepo, "Subject", "project.config", config); db, admin.getIdent(), allProjectsRepo, "Subject", ProjectConfig.PROJECT_CONFIG, config);
push.to(RefNames.REFS_CONFIG).assertOkStatus(); push.to(RefNames.REFS_CONFIG).assertOkStatus();
// Verify that unknownPermission is present // Verify that unknownPermission is present
@@ -498,7 +505,7 @@ public class AccessIT extends AbstractDaemonTest {
gApi.projects() gApi.projects()
.name(allProjects.get()) .name(allProjects.get())
.branch(RefNames.REFS_CONFIG) .branch(RefNames.REFS_CONFIG)
.file("project.config") .file(ProjectConfig.PROJECT_CONFIG)
.asString(); .asString();
cfg.fromText(config); cfg.fromText(config);
assertThat(cfg.getString(access, refsFor, unknownPermission)).isEqualTo(registeredUsers); assertThat(cfg.getString(access, refsFor, unknownPermission)).isEqualTo(registeredUsers);
@@ -517,7 +524,7 @@ public class AccessIT extends AbstractDaemonTest {
gApi.projects() gApi.projects()
.name(allProjects.get()) .name(allProjects.get())
.branch(RefNames.REFS_CONFIG) .branch(RefNames.REFS_CONFIG)
.file("project.config") .file(ProjectConfig.PROJECT_CONFIG)
.asString(); .asString();
cfg.fromText(config); cfg.fromText(config);
assertThat(cfg.getString(access, refsFor, unknownPermission)).isEqualTo(registeredUsers); assertThat(cfg.getString(access, refsFor, unknownPermission)).isEqualTo(registeredUsers);
@@ -540,6 +547,15 @@ public class AccessIT extends AbstractDaemonTest {
gApi.projects().name(project.get()).access(accessInput); gApi.projects().name(project.get()).access(accessInput);
} }
@Test
public void allUsersCanOnlyInheritFromAllProjects() throws Exception {
ProjectAccessInput accessInput = newProjectAccessInput();
accessInput.parent = project.get();
exception.expect(BadRequestException.class);
exception.expectMessage(allUsers.get() + " must inherit from " + allProjects.get());
gApi.projects().name(allUsers.get()).access(accessInput);
}
private ProjectAccessInput newProjectAccessInput() { private ProjectAccessInput newProjectAccessInput() {
ProjectAccessInput p = new ProjectAccessInput(); ProjectAccessInput p = new ProjectAccessInput();
p.add = new HashMap<>(); p.add = new HashMap<>();

View File

@@ -0,0 +1,104 @@
// Copyright (C) 2017 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 static com.google.common.truth.Truth.assertThat;
import com.google.gerrit.extensions.api.GerritApi;
import com.google.gerrit.reviewdb.client.RefNames;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.GerritPersonIdent;
import com.google.gerrit.server.config.AllProjectsName;
import com.google.gerrit.server.config.AllUsersName;
import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
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.testing.SchemaUpgradeTestEnvironment;
import com.google.gerrit.testing.TestUpdateUI;
import com.google.gwtorm.server.OrmException;
import com.google.inject.Inject;
import java.io.IOException;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.lib.ObjectId;
import org.eclipse.jgit.lib.PersonIdent;
import org.eclipse.jgit.lib.Repository;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
public class Schema_161_to_162_Test {
@Rule public SchemaUpgradeTestEnvironment testEnv = new SchemaUpgradeTestEnvironment();
@Inject private AllProjectsName allProjectsName;
@Inject private AllUsersName allUsersName;
@Inject private GerritApi gApi;
@Inject private GitRepositoryManager repoManager;
@Inject private Schema_162 schema162;
@Inject @GerritPersonIdent private PersonIdent serverUser;
private ReviewDb db;
@Before
public void setUp() throws Exception {
testEnv.getInjector().injectMembers(this);
db = testEnv.getDb();
}
@Test
public void skipCorrectInheritance() throws Exception {
assertThatAllUsersInheritsFrom(allProjectsName.get());
ObjectId oldHead;
try (Repository git = repoManager.openRepository(allUsersName)) {
oldHead = git.findRef(RefNames.REFS_CONFIG).getObjectId();
}
schema162.migrateData(db, new TestUpdateUI());
// Check that the parent remained unchanged and that no commit was made
assertThatAllUsersInheritsFrom(allProjectsName.get());
try (Repository git = repoManager.openRepository(allUsersName)) {
assertThat(oldHead).isEqualTo(git.findRef(RefNames.REFS_CONFIG).getObjectId());
}
}
@Test
public void fixIncorrectInheritance() throws Exception {
String testProject = gApi.projects().create("test").get().name;
assertThatAllUsersInheritsFrom(allProjectsName.get());
try (Repository git = repoManager.openRepository(allUsersName);
MetaDataUpdate md = new MetaDataUpdate(GitReferenceUpdated.DISABLED, allUsersName, git)) {
ProjectConfig cfg = ProjectConfig.read(md);
cfg.getProject().setParentName(testProject);
md.getCommitBuilder().setCommitter(serverUser);
md.getCommitBuilder().setAuthor(serverUser);
md.setMessage("Test");
cfg.commit(md);
} catch (ConfigInvalidException | IOException ex) {
throw new OrmException(ex);
}
assertThatAllUsersInheritsFrom(testProject);
schema162.migrateData(db, new TestUpdateUI());
assertThatAllUsersInheritsFrom(allProjectsName.get());
}
private void assertThatAllUsersInheritsFrom(String parent) throws Exception {
assertThat(gApi.projects().name(allUsersName.get()).access().inheritsFrom.name)
.isEqualTo(parent);
}
}