Add capability.administrateServer to gerrit.config

This provides a fail-safe in case an administrator deletes all
administrateServer entries from All-Projects.

In a default installation of Gerrit Code Review all administrators are
locked out and recovery must happen by directly modifying the
refs/meta/config branch in All-Projects "behind Gerrit's back".  In
some hosted installations this option may not be available to the
administrators, as the UNIX account is managed by a different group.

By adding a fail-safe group to gerrit.config the administrators are
always able to use the REST API and web interface to manage the
server, even if all entries were accidentally removed or incorrectly
specified in All-Projects.

Change-Id: I3083dc2997061ce7be095fb53f187856ab8ed0df
This commit is contained in:
Shawn Pearce 2016-08-11 11:51:59 -07:00
parent afbc603265
commit 67a3330950
10 changed files with 226 additions and 43 deletions

View File

@ -892,6 +892,33 @@ threads will die out after the cache is loaded.
+
Default is the number of CPUs.
[[capability]]
=== Section capability
[[capability.administrateServer]]capability.administrateServer::
+
Names of groups of users that are allowed to exercise the
administrateServer capability, in addition to those listed in
All-Projects. Configuring this option can be a useful fail-safe
to recover a server in the event an administrator removed all
groups from the administrateServer capability, or to ensure that
specific groups always have administration capabilities.
+
----
[capability]
administrateServer = group Fail Safe Admins
----
+
The configuration file uses group names, not UUIDs. If a group is
renamed the gerrit.config file must be updated to reflect the new
name. If a group cannot be found for the configured name a warning
is logged and the server will continue normal startup.
+
If not specified (default), only the groups listed by All-Projects
may use the administrateServer capability.
[[change]]
=== Section change

View File

@ -17,6 +17,8 @@ package com.google.gerrit.pgm.util;
import static com.google.inject.Scopes.SINGLETON;
import com.google.common.cache.Cache;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.extensions.api.projects.CommentLinkInfo;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.extensions.registration.DynamicMap;
@ -29,6 +31,7 @@ import com.google.gerrit.server.account.AccountByEmailCacheImpl;
import com.google.gerrit.server.account.AccountCacheImpl;
import com.google.gerrit.server.account.AccountVisibility;
import com.google.gerrit.server.account.AccountVisibilityProvider;
import com.google.gerrit.server.account.CapabilityCollection;
import com.google.gerrit.server.account.CapabilityControl;
import com.google.gerrit.server.account.FakeRealm;
import com.google.gerrit.server.account.GroupCacheImpl;
@ -41,6 +44,7 @@ import com.google.gerrit.server.change.ChangeKindCacheImpl;
import com.google.gerrit.server.change.MergeabilityCacheImpl;
import com.google.gerrit.server.change.PatchSetInserter;
import com.google.gerrit.server.change.RebaseChangeOp;
import com.google.gerrit.server.config.AdministrateServerGroups;
import com.google.gerrit.server.config.CanonicalWebUrl;
import com.google.gerrit.server.config.CanonicalWebUrlProvider;
import com.google.gerrit.server.config.DisableReverseDnsLookup;
@ -130,6 +134,9 @@ public class BatchProgramModule extends FactoryModule {
bind(SearchingChangeCacheImpl.class).toProvider(
Providers.<SearchingChangeCacheImpl>of(null));
bind(new TypeLiteral<ImmutableSet<GroupReference>>() {})
.annotatedWith(AdministrateServerGroups.class)
.toInstance(ImmutableSet.<GroupReference> of());
bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
.annotatedWith(GitUploadPackGroups.class)
.toInstance(Collections.<AccountGroup.UUID> emptySet());
@ -153,6 +160,7 @@ public class BatchProgramModule extends FactoryModule {
install(ChangeKindCacheImpl.module());
install(MergeabilityCacheImpl.module());
install(TagCache.module());
factory(CapabilityCollection.Factory.class);
factory(CapabilityControl.Factory.class);
factory(ChangeData.Factory.class);
factory(ProjectState.Factory.class);

View File

@ -14,32 +14,46 @@
package com.google.gerrit.server.account;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.Nullable;
import com.google.gerrit.common.data.AccessSection;
import com.google.gerrit.common.data.GlobalCapability;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.Permission;
import com.google.gerrit.common.data.PermissionRange;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.server.config.AdministrateServerGroups;
import com.google.gerrit.server.group.SystemGroupBackend;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
/** Caches active {@link GlobalCapability} set for a site. */
public class CapabilityCollection {
private final Map<String, List<PermissionRule>> permissions;
public interface Factory {
CapabilityCollection create(@Nullable AccessSection section);
}
public final List<PermissionRule> administrateServer;
public final List<PermissionRule> batchChangesLimit;
public final List<PermissionRule> emailReviewers;
public final List<PermissionRule> priority;
public final List<PermissionRule> queryLimit;
private final ImmutableMap<String, ImmutableList<PermissionRule>> permissions;
public CapabilityCollection(AccessSection section) {
public final ImmutableList<PermissionRule> administrateServer;
public final ImmutableList<PermissionRule> batchChangesLimit;
public final ImmutableList<PermissionRule> emailReviewers;
public final ImmutableList<PermissionRule> priority;
public final ImmutableList<PermissionRule> queryLimit;
@Inject
CapabilityCollection(
@AdministrateServerGroups ImmutableSet<GroupReference> admins,
@Assisted @Nullable AccessSection section) {
if (section == null) {
section = new AccessSection(AccessSection.GLOBAL_CAPABILITIES);
}
@ -61,18 +75,19 @@ public class CapabilityCollection {
}
}
configureDefaults(tmp, section);
if (!tmp.containsKey(GlobalCapability.ADMINISTRATE_SERVER) && !admins.isEmpty()) {
tmp.put(GlobalCapability.ADMINISTRATE_SERVER, ImmutableList.<PermissionRule>of());
}
Map<String, List<PermissionRule>> res = new HashMap<>();
ImmutableMap.Builder<String, ImmutableList<PermissionRule>> m = ImmutableMap.builder();
for (Map.Entry<String, List<PermissionRule>> e : tmp.entrySet()) {
List<PermissionRule> rules = e.getValue();
if (rules.size() == 1) {
res.put(e.getKey(), Collections.singletonList(rules.get(0)));
} else {
res.put(e.getKey(), Collections.unmodifiableList(
Arrays.asList(rules.toArray(new PermissionRule[rules.size()]))));
if (GlobalCapability.ADMINISTRATE_SERVER.equals(e.getKey())) {
rules = mergeAdmin(admins, rules);
}
m.put(e.getKey(), ImmutableList.copyOf(rules));
}
permissions = Collections.unmodifiableMap(res);
permissions = m.build();
administrateServer = getPermission(GlobalCapability.ADMINISTRATE_SERVER);
batchChangesLimit = getPermission(GlobalCapability.BATCH_CHANGES_LIMIT);
@ -81,9 +96,27 @@ public class CapabilityCollection {
queryLimit = getPermission(GlobalCapability.QUERY_LIMIT);
}
public List<PermissionRule> getPermission(String permissionName) {
List<PermissionRule> r = permissions.get(permissionName);
return r != null ? r : Collections.<PermissionRule> emptyList();
private static List<PermissionRule> mergeAdmin(Set<GroupReference> admins,
List<PermissionRule> rules) {
if (admins.isEmpty()) {
return rules;
}
List<PermissionRule> r = new ArrayList<>(admins.size() + rules.size());
for (GroupReference g : admins) {
r.add(new PermissionRule(g));
}
for (PermissionRule rule : rules) {
if (!admins.contains(rule.getGroup())) {
r.add(rule);
}
}
return r;
}
public ImmutableList<PermissionRule> getPermission(String permissionName) {
ImmutableList<PermissionRule> r = permissions.get(permissionName);
return r != null ? r : ImmutableList.<PermissionRule> of();
}
private static final GroupReference anonymous = SystemGroupBackend

View File

@ -0,0 +1,34 @@
// 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.config;
import static java.lang.annotation.RetentionPolicy.RUNTIME;
import com.google.inject.BindingAnnotation;
import java.lang.annotation.Retention;
/**
* Groups that can always exercise {@code administrateServer} capability.
*
* <pre>
* [capability]
* administrateServer = group Administrators
* </pre>
*/
@Retention(RUNTIME)
@BindingAnnotation
public @interface AdministrateServerGroups {
}

View File

@ -0,0 +1,65 @@
// 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.config;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.common.data.PermissionRule;
import com.google.gerrit.server.account.GroupBackend;
import com.google.gerrit.server.account.GroupBackends;
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.ServerRequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.inject.Inject;
import com.google.inject.Provider;
import org.eclipse.jgit.lib.Config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** Loads {@link AdministrateServerGroups} from {@code gerrit.config}. */
public class AdministrateServerGroupsProvider implements Provider<ImmutableSet<GroupReference>> {
private final ImmutableSet<GroupReference> groups;
@Inject
public AdministrateServerGroupsProvider(GroupBackend groupBackend,
@GerritServerConfig Config config,
ThreadLocalRequestContext threadContext,
ServerRequestContext serverCtx) {
RequestContext ctx = threadContext.setContext(serverCtx);
try {
ImmutableSet.Builder<GroupReference> builder = ImmutableSet.builder();
for (String value : config.getStringList("capability", null, "administrateServer")) {
PermissionRule rule = PermissionRule.fromString(value, false);
String name = rule.getGroup().getName();
GroupReference g = GroupBackends.findBestSuggestion(groupBackend, name);
if (g != null) {
builder.add(g);
} else {
Logger log = LoggerFactory.getLogger(getClass());
log.warn("Group \"{}\" not available, skipping.", name);
}
}
groups = builder.build();
} finally {
threadContext.setContext(ctx);
}
}
@Override
public ImmutableSet<GroupReference> get() {
return groups;
}
}

View File

@ -79,6 +79,7 @@ import com.google.gerrit.server.account.AccountManager;
import com.google.gerrit.server.account.AccountResolver;
import com.google.gerrit.server.account.AccountVisibility;
import com.google.gerrit.server.account.AccountVisibilityProvider;
import com.google.gerrit.server.account.CapabilityCollection;
import com.google.gerrit.server.account.CapabilityControl;
import com.google.gerrit.server.account.ChangeUserName;
import com.google.gerrit.server.account.EmailExpander;
@ -233,6 +234,7 @@ public class GerritGlobalModule extends FactoryModule {
factory(DeleteReviewerSender.Factory.class);
factory(AddKeySender.Factory.class);
factory(BatchUpdate.Factory.class);
factory(CapabilityCollection.Factory.class);
factory(CapabilityControl.Factory.class);
factory(ChangeData.Factory.class);
factory(ChangeJson.Factory.class);

View File

@ -30,10 +30,9 @@ import org.slf4j.LoggerFactory;
import java.util.List;
import java.util.Set;
/** Parses groups referenced in the {@code gerrit.config} file. */
public abstract class GroupSetProvider implements
Provider<Set<AccountGroup.UUID>> {
private static final Logger log =
LoggerFactory.getLogger(GroupSetProvider.class);
protected Set<AccountGroup.UUID> groupIds;
@ -45,10 +44,11 @@ public abstract class GroupSetProvider implements
ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder();
for (String n : groupNames) {
GroupReference g = GroupBackends.findBestSuggestion(groupBackend, n);
if (g == null) {
log.warn("Group \"{}\" not in database, skipping.", n);
} else {
if (g != null) {
builder.add(g.getUUID());
} else {
Logger log = LoggerFactory.getLogger(getClass());
log.warn("Group \"{}\" not available, skipping.", n);
}
}
groupIds = builder.build();

View File

@ -16,8 +16,12 @@ package com.google.gerrit.server.project;
import static com.google.inject.Scopes.SINGLETON;
import com.google.common.collect.ImmutableSet;
import com.google.gerrit.common.data.GroupReference;
import com.google.gerrit.extensions.config.FactoryModule;
import com.google.gerrit.reviewdb.client.AccountGroup;
import com.google.gerrit.server.config.AdministrateServerGroups;
import com.google.gerrit.server.config.AdministrateServerGroupsProvider;
import com.google.gerrit.server.config.GitReceivePackGroups;
import com.google.gerrit.server.config.GitReceivePackGroupsProvider;
import com.google.gerrit.server.config.GitUploadPackGroups;
@ -29,13 +33,20 @@ import java.util.Set;
public class AccessControlModule extends FactoryModule {
@Override
protected void configure() {
bind(new TypeLiteral<Set<AccountGroup.UUID>>() {}) //
.annotatedWith(GitUploadPackGroups.class) //
.toProvider(GitUploadPackGroupsProvider.class).in(SINGLETON);
bind(new TypeLiteral<ImmutableSet<GroupReference>>() {})
.annotatedWith(AdministrateServerGroups.class)
.toProvider(AdministrateServerGroupsProvider.class)
.in(SINGLETON);
bind(new TypeLiteral<Set<AccountGroup.UUID>>() {}) //
.annotatedWith(GitReceivePackGroups.class) //
.toProvider(GitReceivePackGroupsProvider.class).in(SINGLETON);
bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
.annotatedWith(GitUploadPackGroups.class)
.toProvider(GitUploadPackGroupsProvider.class)
.in(SINGLETON);
bind(new TypeLiteral<Set<AccountGroup.UUID>>() {})
.annotatedWith(GitReceivePackGroups.class)
.toProvider(GitReceivePackGroupsProvider.class)
.in(SINGLETON);
bind(ChangeControl.Factory.class);
factory(ProjectControl.AssistedFactory.class);

View File

@ -123,6 +123,7 @@ public class ProjectState {
final GitRepositoryManager gitMgr,
final RulesCache rulesCache,
final List<CommentLinkInfo> commentLinks,
final CapabilityCollection.Factory capabilityFactory,
@Assisted final ProjectConfig config) {
this.sitePaths = sitePaths;
this.projectCache = projectCache;
@ -137,7 +138,7 @@ public class ProjectState {
this.config = config;
this.configs = new HashMap<>();
this.capabilities = isAllProjects
? new CapabilityCollection(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES))
? capabilityFactory.create(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES))
: null;
if (isAllProjects && !Permission.canBeOnAllProjects(AccessSection.ALL, Permission.OWNER)) {

View File

@ -47,6 +47,7 @@ import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.rules.PrologEnvironment;
import com.google.gerrit.rules.RulesCache;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.account.CapabilityCollection;
import com.google.gerrit.server.account.CapabilityControl;
import com.google.gerrit.server.account.GroupMembership;
import com.google.gerrit.server.account.ListGroupMembership;
@ -235,6 +236,7 @@ public class RefControlTest {
private ChangeControl.Factory changeControlFactory;
private ReviewDb db;
@Inject private CapabilityCollection.Factory capabilityCollectionFactory;
@Inject private CapabilityControl.Factory capabilityControlFactory;
@Inject private SchemaCreator schemaCreator;
@Inject private InMemoryDatabase schemaFactory;
@ -243,18 +245,6 @@ public class RefControlTest {
@Before
public void setUp() throws Exception {
repoManager = new InMemoryRepositoryManager();
try {
Repository repo = repoManager.createRepository(allProjectsName);
ProjectConfig allProjects =
new ProjectConfig(new Project.NameKey(allProjectsName.get()));
allProjects.load(repo);
LabelType cr = Util.codeReview();
allProjects.getLabelSections().put(cr.getName(), cr);
add(allProjects);
} catch (IOException | ConfigInvalidException e) {
throw new RuntimeException(e);
}
projectCache = new ProjectCache() {
@Override
public ProjectState getAllProjects() {
@ -312,6 +302,18 @@ public class RefControlTest {
Injector injector = Guice.createInjector(new InMemoryModule());
injector.injectMembers(this);
try {
Repository repo = repoManager.createRepository(allProjectsName);
ProjectConfig allProjects =
new ProjectConfig(new Project.NameKey(allProjectsName.get()));
allProjects.load(repo);
LabelType cr = Util.codeReview();
allProjects.getLabelSections().put(cr.getName(), cr);
add(allProjects);
} catch (IOException | ConfigInvalidException e) {
throw new RuntimeException(e);
}
db = schemaFactory.open();
schemaCreator.create(db);
@ -865,7 +867,7 @@ public class RefControlTest {
all.put(pc.getName(),
new ProjectState(sitePaths, projectCache, allProjectsName, allUsersName,
projectControlFactory, envFactory, repoManager, rulesCache,
commentLinks, pc));
commentLinks, capabilityCollectionFactory, pc));
return repo;
}