Allow plugins to store encypted settings in etc/$PLUGIN.secure.config

Change-Id: I82dd2fdd33c53aa2db7cb136c79548e0fc8d0ede
This commit is contained in:
David Pursehouse
2016-07-22 11:00:25 +09:00
parent 9d2fbc1ddb
commit 5b47bc4d65
6 changed files with 147 additions and 6 deletions

View File

@@ -737,6 +737,18 @@ this:
reviewer = My Info Developers
----
Plugins that have sensitive configuration settings can store those settings in
an own secure configuration file. The plugin's secure configuration file must be
named after the plugin and must be located in the `etc` folder of the review
site. For example a secure configuration file for a `default-reviewer` plugin
could look like this:
.$site_path/etc/default-reviewer.secure.config
----
[auth]
password = secret
----
Via the `com.google.gerrit.server.config.PluginConfigFactory` class a
plugin can easily access its configuration:
@@ -749,6 +761,8 @@ private com.google.gerrit.server.config.PluginConfigFactory cfg;
String[] reviewers = cfg.getGlobalPluginConfig("default-reviewer")
.getStringList("branch", "refs/heads/master", "reviewer");
String password = cfg.getGlobalPluginConfig("default-reviewer")
.getString("auth", null, "password");
----

View File

@@ -127,6 +127,12 @@ public class UpgradeFrom2_0_xTest extends InitTestCase {
return cfg.getStringList(section, subsection, name);
}
@Override
public String[] getListForPlugin(String pluginName, String section,
String subsection, String name) {
throw new UnsupportedOperationException("not used by tests");
}
@Override
public void setList(String section, String subsection, String name,
List<String> values) {

View File

@@ -0,0 +1,54 @@
// Copyright (C) 2016 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.gerrit.server.securestore.SecureStore;
import org.eclipse.jgit.lib.Config;
/**
* Plugin configuration in etc/$PLUGIN.config and etc/$PLUGIN.secure.config.
*/
public class GlobalPluginConfig extends Config {
private final SecureStore secureStore;
private final String pluginName;
GlobalPluginConfig(String pluginName, Config baseConfig,
SecureStore secureStore) {
super(baseConfig);
this.pluginName = pluginName;
this.secureStore = secureStore;
}
@Override
public String getString(String section, String subsection, String name) {
String secure = secureStore.getForPlugin(
pluginName, section, subsection, name);
if (secure != null) {
return secure;
}
return super.getString(section, subsection, name);
}
@Override
public String[] getStringList(String section, String subsection, String name) {
String[] secure = secureStore.getListForPlugin(
pluginName, section, subsection, name);
if (secure != null && secure.length > 0) {
return secure;
}
return super.getStringList(section, subsection, name);
}
}

View File

@@ -21,6 +21,7 @@ import com.google.gerrit.server.plugins.ReloadPluginListener;
import com.google.gerrit.server.project.NoSuchProjectException;
import com.google.gerrit.server.project.ProjectCache;
import com.google.gerrit.server.project.ProjectState;
import com.google.gerrit.server.securestore.SecureStore;
import com.google.inject.Inject;
import com.google.inject.Provider;
import com.google.inject.Singleton;
@@ -49,6 +50,7 @@ public class PluginConfigFactory implements ReloadPluginListener {
private final Provider<Config> cfgProvider;
private final ProjectCache projectCache;
private final ProjectState.Factory projectStateFactory;
private final SecureStore secureStore;
private final Map<String, Config> pluginConfigs;
private volatile FileSnapshot cfgSnapshot;
@@ -59,13 +61,15 @@ public class PluginConfigFactory implements ReloadPluginListener {
SitePaths site,
@GerritServerConfig Provider<Config> cfgProvider,
ProjectCache projectCache,
ProjectState.Factory projectStateFactory) {
ProjectState.Factory projectStateFactory,
SecureStore secureStore) {
this.site = site;
this.cfgProvider = cfgProvider;
this.projectCache = projectCache;
this.projectStateFactory = projectStateFactory;
this.pluginConfigs = new HashMap<>();
this.secureStore = secureStore;
this.pluginConfigs = new HashMap<>();
this.cfgSnapshot = FileSnapshot.save(site.gerrit_config.toFile());
this.cfg = cfgProvider.get();
}
@@ -260,10 +264,12 @@ public class PluginConfigFactory implements ReloadPluginListener {
Path pluginConfigFile = site.etc_dir.resolve(pluginName + ".config");
FileBasedConfig cfg =
new FileBasedConfig(pluginConfigFile.toFile(), FS.DETECTED);
pluginConfigs.put(pluginName, cfg);
GlobalPluginConfig pluginConfig =
new GlobalPluginConfig(pluginName, cfg, secureStore);
pluginConfigs.put(pluginName, pluginConfig);
if (!cfg.getFile().exists()) {
log.info("No " + pluginConfigFile.toAbsolutePath() + "; assuming defaults");
return cfg;
return pluginConfig;
}
try {
@@ -272,7 +278,7 @@ public class PluginConfigFactory implements ReloadPluginListener {
log.warn("Failed to load " + pluginConfigFile.toAbsolutePath(), e);
}
return cfg;
return pluginConfig;
}
/**

View File

@@ -19,6 +19,7 @@ import com.google.gerrit.server.config.SitePaths;
import com.google.inject.Inject;
import com.google.inject.Singleton;
import org.eclipse.jgit.errors.ConfigInvalidException;
import org.eclipse.jgit.internal.storage.file.LockFile;
import org.eclipse.jgit.lib.Constants;
import org.eclipse.jgit.storage.file.FileBasedConfig;
@@ -27,20 +28,26 @@ import org.eclipse.jgit.util.FS;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Singleton
public class DefaultSecureStore extends SecureStore {
private final FileBasedConfig sec;
private final Map<String, FileBasedConfig> pluginSec;
private final SitePaths site;
@Inject
DefaultSecureStore(SitePaths site) {
this.site = site;
sec = new FileBasedConfig(site.secure_config.toFile(), FS.DETECTED);
try {
sec.load();
} catch (Exception e) {
} catch (IOException | ConfigInvalidException e) {
throw new RuntimeException("Cannot load secure.config", e);
}
this.pluginSec = new HashMap<>();
}
@Override
@@ -48,6 +55,28 @@ public class DefaultSecureStore extends SecureStore {
return sec.getStringList(section, subsection, name);
}
@Override
public synchronized String[] getListForPlugin(String pluginName, String section,
String subsection, String name) {
FileBasedConfig cfg = null;
if (pluginSec.containsKey(pluginName)) {
cfg = pluginSec.get(pluginName);
} else {
String filename = pluginName + ".secure.config";
File pluginConfigFile = site.etc_dir.resolve(filename).toFile();
if (pluginConfigFile.exists()) {
cfg = new FileBasedConfig(pluginConfigFile, FS.DETECTED);
try {
cfg.load();
pluginSec.put(pluginName, cfg);
} catch (IOException | ConfigInvalidException e) {
throw new RuntimeException("Cannot load " + filename, e);
}
}
}
return cfg != null ? cfg.getStringList(section, subsection, name) : null;
}
@Override
public void setList(String section, String subsection, String name,
List<String> values) {

View File

@@ -74,6 +74,38 @@ public abstract class SecureStore {
return null;
}
/**
* Extract decrypted value of stored plugin config property from SecureStore
* or {@code null} when property was not found.
*
* @param pluginName
* @param section
* @param subsection
* @param name
* @return decrypted String value or {@code null} if not found
*/
public final String getForPlugin(String pluginName, String section,
String subsection, String name) {
String[] values = getListForPlugin(pluginName, section, subsection, name);
if (values != null && values.length > 0) {
return values[0];
}
return null;
}
/**
* Extract list of plugin config values from SecureStore and decrypt every
* value in that list, or {@code null} when property was not found.
*
* @param pluginName
* @param section
* @param subsection
* @param name
* @return decrypted list of string values or {@code null}
*/
public abstract String[] getListForPlugin(String pluginName, String section,
String subsection, String name);
/**
* Extract list of values from SecureStore and decrypt every value in that
* list or {@code null} when property was not found.