Run plugin startup and shutdown as PluginUser

When a plugin is loaded at server startup the request context is
pointing to an AnonymousUser instance.  This prevents plugins from
resolving internal groups that are not anonymously visible.  Instead
set the request context to be an instance of PluginUser, which extends
InternalUser, a privileged type that can see any group.

This also fixes the inconsistent behavior when reloading plugins.
Plugins always start with the same PluginUser identity and never
inherit the identity of the caller performing the reload or install.

Bug: issue 1827
Change-Id: Icf9991bebefce3239048f902f828473f07dfc284
This commit is contained in:
Shawn Pearce
2013-04-23 17:42:48 -07:00
parent 526e021526
commit 84717f6449
7 changed files with 166 additions and 18 deletions

View File

@@ -31,6 +31,8 @@ import java.util.Set;
* anything it wants, anytime it wants, given the JVM's own direct access to
* data. Plugins may use this when they need to have a CurrentUser with read
* permission on anything.
*
* @see PluginUser
*/
public class InternalUser extends CurrentUser {
public interface Factory {

View File

@@ -0,0 +1,46 @@
// Copyright (C) 2013 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;
import com.google.gerrit.server.account.CapabilityControl;
import com.google.inject.Inject;
import com.google.inject.assistedinject.Assisted;
/** User identity for plugin code that needs an identity. */
public class PluginUser extends InternalUser {
public interface Factory {
PluginUser create(String pluginName);
}
private final String pluginName;
@Inject
protected PluginUser(
CapabilityControl.Factory capabilityControlFactory,
@Assisted String pluginName) {
super(capabilityControlFactory);
this.pluginName = pluginName;
}
@Override
public String getUserName() {
return "plugin " + pluginName;
}
@Override
public String toString() {
return "PluginUser[" + pluginName + "]";
}
}

View File

@@ -32,6 +32,7 @@ import com.google.gerrit.server.FileTypeRegistry;
import com.google.gerrit.server.IdentifiedUser;
import com.google.gerrit.server.InternalUser;
import com.google.gerrit.server.MimeUtilFileTypeRegistry;
import com.google.gerrit.server.PluginUser;
import com.google.gerrit.server.account.AccountByEmailCacheImpl;
import com.google.gerrit.server.account.AccountCacheImpl;
import com.google.gerrit.server.account.AccountControl;
@@ -181,6 +182,7 @@ public class GerritGlobalModule extends FactoryModule {
factory(MergeUtil.Factory.class);
factory(PerformCreateGroup.Factory.class);
factory(PerformRenameGroup.Factory.class);
factory(PluginUser.Factory.class);
factory(ProjectNode.Factory.class);
factory(ProjectState.Factory.class);
factory(RebasedPatchSetSender.Factory.class);

View File

@@ -21,6 +21,8 @@ import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
import com.google.gerrit.lifecycle.LifecycleManager;
import com.google.gerrit.server.PluginUser;
import com.google.gerrit.server.util.RequestContext;
import com.google.inject.AbstractModule;
import com.google.inject.Guice;
import com.google.inject.Injector;
@@ -83,6 +85,7 @@ public class Plugin {
private final CacheKey cacheKey;
private final String name;
private final PluginUser pluginUser;
private final File srcJar;
private final FileSnapshot snapshot;
private final JarFile jarFile;
@@ -102,6 +105,7 @@ public class Plugin {
private List<ReloadableRegistrationHandle<?>> reloadableHandles;
public Plugin(String name,
PluginUser pluginUser,
File srcJar,
FileSnapshot snapshot,
JarFile jarFile,
@@ -113,6 +117,7 @@ public class Plugin {
@Nullable Class<? extends Module> sshModule,
@Nullable Class<? extends Module> httpModule) {
this.cacheKey = new CacheKey(name);
this.pluginUser = pluginUser;
this.name = name;
this.srcJar = srcJar;
this.snapshot = snapshot;
@@ -131,6 +136,10 @@ public class Plugin {
return srcJar;
}
PluginUser getPluginUser() {
return pluginUser;
}
public CacheKey getCacheKey() {
return cacheKey;
}
@@ -173,6 +182,15 @@ public class Plugin {
}
void start(PluginGuiceEnvironment env) throws Exception {
RequestContext oldContext = env.enter(this);
try {
startPlugin(env);
} finally {
env.exit(oldContext);
}
}
private void startPlugin(PluginGuiceEnvironment env) throws Exception {
Injector root = newRootInjector(env);
manager = new LifecycleManager();
@@ -235,6 +253,7 @@ public class Plugin {
modules.add(new AbstractModule() {
@Override
protected void configure() {
bind(PluginUser.class).toInstance(pluginUser);
bind(String.class)
.annotatedWith(PluginName.class)
.toInstance(name);
@@ -264,9 +283,14 @@ public class Plugin {
return Guice.createInjector(modules);
}
void stop() {
void stop(PluginGuiceEnvironment env) {
if (manager != null) {
manager.stop();
RequestContext oldContext = env.enter(this);
try {
manager.stop();
} finally {
env.exit(oldContext);
}
manager = null;
sysInjector = null;
sshInjector = null;

View File

@@ -32,6 +32,9 @@ import com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes;
import com.google.gerrit.extensions.registration.RegistrationHandle;
import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
import com.google.gerrit.extensions.systemstatus.ServerInformation;
import com.google.gerrit.server.util.PluginRequestContext;
import com.google.gerrit.server.util.RequestContext;
import com.google.gerrit.server.util.ThreadLocalRequestContext;
import com.google.inject.AbstractModule;
import com.google.inject.Binding;
import com.google.inject.Guice;
@@ -65,6 +68,7 @@ import javax.inject.Inject;
public class PluginGuiceEnvironment {
private final Injector sysInjector;
private final ServerInformation srvInfo;
private final ThreadLocalRequestContext local;
private final CopyConfigModule copyConfigModule;
private final Set<Key<?>> copyConfigKeys;
private final List<StartPluginListener> onStart;
@@ -90,10 +94,12 @@ public class PluginGuiceEnvironment {
@Inject
PluginGuiceEnvironment(
Injector sysInjector,
ThreadLocalRequestContext local,
ServerInformation srvInfo,
CopyConfigModule ccm) {
this.sysInjector = sysInjector;
this.srvInfo = srvInfo;
this.local = local;
this.copyConfigModule = ccm;
this.copyConfigKeys = Guice.createInjector(ccm).getAllBindings().keySet();
@@ -187,20 +193,33 @@ public class PluginGuiceEnvironment {
return httpGen.get();
}
RequestContext enter(Plugin plugin) {
return local.setContext(new PluginRequestContext(plugin.getPluginUser()));
}
void exit(RequestContext old) {
local.setContext(old);
}
void onStartPlugin(Plugin plugin) {
for (StartPluginListener l : onStart) {
l.onStartPlugin(plugin);
}
attachItem(sysItems, plugin.getSysInjector(), plugin);
RequestContext oldContext = enter(plugin);
try {
attachItem(sysItems, plugin.getSysInjector(), plugin);
attachSet(sysSets, plugin.getSysInjector(), plugin);
attachSet(sshSets, plugin.getSshInjector(), plugin);
attachSet(httpSets, plugin.getHttpInjector(), plugin);
attachSet(sysSets, plugin.getSysInjector(), plugin);
attachSet(sshSets, plugin.getSshInjector(), plugin);
attachSet(httpSets, plugin.getHttpInjector(), plugin);
attachMap(sysMaps, plugin.getSysInjector(), plugin);
attachMap(sshMaps, plugin.getSshInjector(), plugin);
attachMap(httpMaps, plugin.getHttpInjector(), plugin);
attachMap(sysMaps, plugin.getSysInjector(), plugin);
attachMap(sshMaps, plugin.getSshInjector(), plugin);
attachMap(httpMaps, plugin.getHttpInjector(), plugin);
} finally {
exit(oldContext);
}
}
private void attachItem(Map<TypeLiteral<?>, DynamicItem<?>> items,
@@ -244,15 +263,20 @@ public class PluginGuiceEnvironment {
old.put(h.getKey().getTypeLiteral(), h);
}
reattachMap(old, sysMaps, newPlugin.getSysInjector(), newPlugin);
reattachMap(old, sshMaps, newPlugin.getSshInjector(), newPlugin);
reattachMap(old, httpMaps, newPlugin.getHttpInjector(), newPlugin);
RequestContext oldContext = enter(newPlugin);
try {
reattachMap(old, sysMaps, newPlugin.getSysInjector(), newPlugin);
reattachMap(old, sshMaps, newPlugin.getSshInjector(), newPlugin);
reattachMap(old, httpMaps, newPlugin.getHttpInjector(), newPlugin);
reattachSet(old, sysSets, newPlugin.getSysInjector(), newPlugin);
reattachSet(old, sshSets, newPlugin.getSshInjector(), newPlugin);
reattachSet(old, httpSets, newPlugin.getHttpInjector(), newPlugin);
reattachSet(old, sysSets, newPlugin.getSysInjector(), newPlugin);
reattachSet(old, sshSets, newPlugin.getSshInjector(), newPlugin);
reattachSet(old, httpSets, newPlugin.getHttpInjector(), newPlugin);
reattachItem(old, sysItems, newPlugin.getSysInjector(), newPlugin);
reattachItem(old, sysItems, newPlugin.getSysInjector(), newPlugin);
} finally {
exit(oldContext);
}
}
private void reattachMap(

View File

@@ -24,6 +24,7 @@ import com.google.gerrit.extensions.annotations.PluginName;
import com.google.gerrit.extensions.events.LifecycleListener;
import com.google.gerrit.extensions.systemstatus.ServerInformation;
import com.google.gerrit.extensions.webui.JavaScriptPlugin;
import com.google.gerrit.server.PluginUser;
import com.google.gerrit.server.config.ConfigUtil;
import com.google.gerrit.server.config.GerritServerConfig;
import com.google.gerrit.server.config.SitePaths;
@@ -72,6 +73,7 @@ public class PluginLoader implements LifecycleListener {
private final File tmpDir;
private final PluginGuiceEnvironment env;
private final ServerInformationImpl srvInfoImpl;
private final PluginUser.Factory pluginUserFactory;
private final ConcurrentMap<String, Plugin> running;
private final ConcurrentMap<String, Plugin> disabled;
private final Map<String, FileSnapshot> broken;
@@ -84,6 +86,7 @@ public class PluginLoader implements LifecycleListener {
public PluginLoader(SitePaths sitePaths,
PluginGuiceEnvironment pe,
ServerInformationImpl sii,
PluginUser.Factory puf,
Provider<PluginCleanerTask> pct,
@GerritServerConfig Config cfg) {
pluginsDir = sitePaths.plugins_dir;
@@ -91,6 +94,7 @@ public class PluginLoader implements LifecycleListener {
tmpDir = sitePaths.tmp_dir;
env = pe;
srvInfoImpl = sii;
pluginUserFactory = puf;
running = Maps.newConcurrentMap();
disabled = Maps.newConcurrentMap();
broken = Maps.newHashMap();
@@ -194,7 +198,7 @@ public class PluginLoader implements LifecycleListener {
synchronized private void unloadPlugin(Plugin plugin) {
String name = plugin.getName();
log.info(String.format("Unloading plugin %s", name));
plugin.stop();
plugin.stop(env);
running.remove(name);
disabled.remove(name);
toCleanup.add(plugin);
@@ -466,7 +470,7 @@ public class PluginLoader implements LifecycleListener {
Class<? extends Module> sysModule = load(sysName, pluginLoader);
Class<? extends Module> sshModule = load(sshName, pluginLoader);
Class<? extends Module> httpModule = load(httpName, pluginLoader);
Plugin plugin = new Plugin(name,
Plugin plugin = new Plugin(name, pluginUserFactory.create(name),
srcJar, snapshot,
jarFile, manifest,
new File(dataDir, name), type, pluginLoader,

View File

@@ -0,0 +1,46 @@
// Copyright (C) 2013 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.util;
import com.google.gerrit.reviewdb.server.ReviewDb;
import com.google.gerrit.server.CurrentUser;
import com.google.gerrit.server.PluginUser;
import com.google.inject.Provider;
import com.google.inject.ProvisionException;
/** RequestContext active while plugins load or unload. */
public class PluginRequestContext implements RequestContext {
private final PluginUser user;
public PluginRequestContext(PluginUser user) {
this.user = user;
}
@Override
public CurrentUser getCurrentUser() {
return user;
}
@Override
public Provider<ReviewDb> getReviewDbProvider() {
return new Provider<ReviewDb>() {
@Override
public ReviewDb get() {
throw new ProvisionException(
"Automatic ReviewDb only available in request scope");
}
};
}
}