Allow plugins to store encypted settings in etc/$PLUGIN.secure.config
Change-Id: I82dd2fdd33c53aa2db7cb136c79548e0fc8d0ede
This commit is contained in:
@@ -737,6 +737,18 @@ this:
|
|||||||
reviewer = My Info Developers
|
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
|
Via the `com.google.gerrit.server.config.PluginConfigFactory` class a
|
||||||
plugin can easily access its configuration:
|
plugin can easily access its configuration:
|
||||||
|
|
||||||
@@ -749,6 +761,8 @@ private com.google.gerrit.server.config.PluginConfigFactory cfg;
|
|||||||
|
|
||||||
String[] reviewers = cfg.getGlobalPluginConfig("default-reviewer")
|
String[] reviewers = cfg.getGlobalPluginConfig("default-reviewer")
|
||||||
.getStringList("branch", "refs/heads/master", "reviewer");
|
.getStringList("branch", "refs/heads/master", "reviewer");
|
||||||
|
String password = cfg.getGlobalPluginConfig("default-reviewer")
|
||||||
|
.getString("auth", null, "password");
|
||||||
----
|
----
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -127,6 +127,12 @@ public class UpgradeFrom2_0_xTest extends InitTestCase {
|
|||||||
return cfg.getStringList(section, subsection, name);
|
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
|
@Override
|
||||||
public void setList(String section, String subsection, String name,
|
public void setList(String section, String subsection, String name,
|
||||||
List<String> values) {
|
List<String> values) {
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -21,6 +21,7 @@ import com.google.gerrit.server.plugins.ReloadPluginListener;
|
|||||||
import com.google.gerrit.server.project.NoSuchProjectException;
|
import com.google.gerrit.server.project.NoSuchProjectException;
|
||||||
import com.google.gerrit.server.project.ProjectCache;
|
import com.google.gerrit.server.project.ProjectCache;
|
||||||
import com.google.gerrit.server.project.ProjectState;
|
import com.google.gerrit.server.project.ProjectState;
|
||||||
|
import com.google.gerrit.server.securestore.SecureStore;
|
||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Provider;
|
import com.google.inject.Provider;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
@@ -49,6 +50,7 @@ public class PluginConfigFactory implements ReloadPluginListener {
|
|||||||
private final Provider<Config> cfgProvider;
|
private final Provider<Config> cfgProvider;
|
||||||
private final ProjectCache projectCache;
|
private final ProjectCache projectCache;
|
||||||
private final ProjectState.Factory projectStateFactory;
|
private final ProjectState.Factory projectStateFactory;
|
||||||
|
private final SecureStore secureStore;
|
||||||
private final Map<String, Config> pluginConfigs;
|
private final Map<String, Config> pluginConfigs;
|
||||||
|
|
||||||
private volatile FileSnapshot cfgSnapshot;
|
private volatile FileSnapshot cfgSnapshot;
|
||||||
@@ -59,13 +61,15 @@ public class PluginConfigFactory implements ReloadPluginListener {
|
|||||||
SitePaths site,
|
SitePaths site,
|
||||||
@GerritServerConfig Provider<Config> cfgProvider,
|
@GerritServerConfig Provider<Config> cfgProvider,
|
||||||
ProjectCache projectCache,
|
ProjectCache projectCache,
|
||||||
ProjectState.Factory projectStateFactory) {
|
ProjectState.Factory projectStateFactory,
|
||||||
|
SecureStore secureStore) {
|
||||||
this.site = site;
|
this.site = site;
|
||||||
this.cfgProvider = cfgProvider;
|
this.cfgProvider = cfgProvider;
|
||||||
this.projectCache = projectCache;
|
this.projectCache = projectCache;
|
||||||
this.projectStateFactory = projectStateFactory;
|
this.projectStateFactory = projectStateFactory;
|
||||||
this.pluginConfigs = new HashMap<>();
|
this.secureStore = secureStore;
|
||||||
|
|
||||||
|
this.pluginConfigs = new HashMap<>();
|
||||||
this.cfgSnapshot = FileSnapshot.save(site.gerrit_config.toFile());
|
this.cfgSnapshot = FileSnapshot.save(site.gerrit_config.toFile());
|
||||||
this.cfg = cfgProvider.get();
|
this.cfg = cfgProvider.get();
|
||||||
}
|
}
|
||||||
@@ -260,10 +264,12 @@ public class PluginConfigFactory implements ReloadPluginListener {
|
|||||||
Path pluginConfigFile = site.etc_dir.resolve(pluginName + ".config");
|
Path pluginConfigFile = site.etc_dir.resolve(pluginName + ".config");
|
||||||
FileBasedConfig cfg =
|
FileBasedConfig cfg =
|
||||||
new FileBasedConfig(pluginConfigFile.toFile(), FS.DETECTED);
|
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()) {
|
if (!cfg.getFile().exists()) {
|
||||||
log.info("No " + pluginConfigFile.toAbsolutePath() + "; assuming defaults");
|
log.info("No " + pluginConfigFile.toAbsolutePath() + "; assuming defaults");
|
||||||
return cfg;
|
return pluginConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
@@ -272,7 +278,7 @@ public class PluginConfigFactory implements ReloadPluginListener {
|
|||||||
log.warn("Failed to load " + pluginConfigFile.toAbsolutePath(), e);
|
log.warn("Failed to load " + pluginConfigFile.toAbsolutePath(), e);
|
||||||
}
|
}
|
||||||
|
|
||||||
return cfg;
|
return pluginConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import com.google.gerrit.server.config.SitePaths;
|
|||||||
import com.google.inject.Inject;
|
import com.google.inject.Inject;
|
||||||
import com.google.inject.Singleton;
|
import com.google.inject.Singleton;
|
||||||
|
|
||||||
|
import org.eclipse.jgit.errors.ConfigInvalidException;
|
||||||
import org.eclipse.jgit.internal.storage.file.LockFile;
|
import org.eclipse.jgit.internal.storage.file.LockFile;
|
||||||
import org.eclipse.jgit.lib.Constants;
|
import org.eclipse.jgit.lib.Constants;
|
||||||
import org.eclipse.jgit.storage.file.FileBasedConfig;
|
import org.eclipse.jgit.storage.file.FileBasedConfig;
|
||||||
@@ -27,20 +28,26 @@ import org.eclipse.jgit.util.FS;
|
|||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
@Singleton
|
@Singleton
|
||||||
public class DefaultSecureStore extends SecureStore {
|
public class DefaultSecureStore extends SecureStore {
|
||||||
private final FileBasedConfig sec;
|
private final FileBasedConfig sec;
|
||||||
|
private final Map<String, FileBasedConfig> pluginSec;
|
||||||
|
private final SitePaths site;
|
||||||
|
|
||||||
@Inject
|
@Inject
|
||||||
DefaultSecureStore(SitePaths site) {
|
DefaultSecureStore(SitePaths site) {
|
||||||
|
this.site = site;
|
||||||
sec = new FileBasedConfig(site.secure_config.toFile(), FS.DETECTED);
|
sec = new FileBasedConfig(site.secure_config.toFile(), FS.DETECTED);
|
||||||
try {
|
try {
|
||||||
sec.load();
|
sec.load();
|
||||||
} catch (Exception e) {
|
} catch (IOException | ConfigInvalidException e) {
|
||||||
throw new RuntimeException("Cannot load secure.config", e);
|
throw new RuntimeException("Cannot load secure.config", e);
|
||||||
}
|
}
|
||||||
|
this.pluginSec = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -48,6 +55,28 @@ public class DefaultSecureStore extends SecureStore {
|
|||||||
return sec.getStringList(section, subsection, name);
|
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
|
@Override
|
||||||
public void setList(String section, String subsection, String name,
|
public void setList(String section, String subsection, String name,
|
||||||
List<String> values) {
|
List<String> values) {
|
||||||
|
|||||||
@@ -74,6 +74,38 @@ public abstract class SecureStore {
|
|||||||
return null;
|
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
|
* Extract list of values from SecureStore and decrypt every value in that
|
||||||
* list or {@code null} when property was not found.
|
* list or {@code null} when property was not found.
|
||||||
|
|||||||
Reference in New Issue
Block a user