Integration with license plugin

- Added a `LicenseService` to handle license feature enable/disable events
 - LicenseEventNotifier is responsible for notifying the license events to whatever registered listeners that are interested in them
 - In Shield, when a license is disabled for `shield` feature, we block all read operations (done in the `ShieldActionFilter`)
 - Added initial documentation around licensing

Closes elastic/elasticsearch#347

Original commit: elastic/x-pack-elasticsearch@6ba7a10cd4
This commit is contained in:
uboness 2014-11-14 14:34:30 -08:00
parent 070dbebb7a
commit e646fd5edc
17 changed files with 590 additions and 43 deletions

23
pom.xml
View File

@ -17,8 +17,19 @@
<!-- needed for some test features for now, remove with 1.3 release -->
<repositories>
<repository>
<id>oss.sonatype.org-snapshot</id>
<url>http://oss.sonatype.org/content/repositories/snapshots</url>
<id>es-releases</id>
<url>http://maven.elasticsearch.org/libs-release-local</url>
<releases>
<enabled>true</enabled>
<updatePolicy>daily</updatePolicy>
</releases>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
<repository>
<id>es-snapshots</id>
<url>http://maven.elasticsearch.org/libs-snapshot</url>
<releases>
<enabled>false</enabled>
</releases>
@ -33,6 +44,7 @@
<lucene.version>4.10.2</lucene.version>
<lucene.maven.version>4.10.2</lucene.maven.version>
<elasticsearch.version>1.4.0</elasticsearch.version>
<license.plugin.version>1.0.0-beta1</license.plugin.version>
<tests.jvms>auto</tests.jvms>
<tests.shuffle>true</tests.shuffle>
@ -150,6 +162,13 @@
<version>1.9</version>
</dependency>
<dependency>
<groupId>org.elasticsearch</groupId>
<artifactId>elasticsearch-license-plugin</artifactId>
<version>${license.plugin.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>

View File

@ -14,6 +14,7 @@ import org.elasticsearch.shield.authc.AuthenticationModule;
import org.elasticsearch.shield.authz.AuthorizationModule;
import org.elasticsearch.shield.signature.SignatureModule;
import org.elasticsearch.shield.rest.ShieldRestModule;
import org.elasticsearch.shield.license.LicenseModule;
import org.elasticsearch.shield.ssl.SSLModule;
import org.elasticsearch.shield.support.AbstractShieldModule;
import org.elasticsearch.shield.transport.SecuredTransportModule;
@ -23,20 +24,13 @@ import org.elasticsearch.shield.transport.SecuredTransportModule;
*/
public class ShieldModule extends AbstractShieldModule.Spawn {
private final boolean enabled;
public ShieldModule(Settings settings) {
super(settings);
this.enabled = settings.getAsBoolean("shield.enabled", true);
}
@Override
public Iterable<? extends Module> spawnModules(boolean clientMode) {
// don't spawn modules if shield is explicitly disabled
if (!enabled) {
return ImmutableList.of();
}
// spawn needed parts in client mode
if (clientMode) {
return ImmutableList.<Module>of(
@ -45,6 +39,7 @@ public class ShieldModule extends AbstractShieldModule.Spawn {
}
return ImmutableList.<Module>of(
new LicenseModule(settings),
new AuthenticationModule(settings),
new AuthorizationModule(settings),
new AuditTrailModule(settings),

View File

@ -5,17 +5,18 @@
*/
package org.elasticsearch.shield;
import org.elasticsearch.client.Client;
import org.elasticsearch.client.support.Headers;
import org.elasticsearch.common.collect.ImmutableList;
import org.elasticsearch.common.component.LifecycleComponent;
import org.elasticsearch.common.inject.Module;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.env.Environment;
import org.elasticsearch.plugins.AbstractPlugin;
import org.elasticsearch.shield.ShieldModule;
import org.elasticsearch.shield.ShieldSettingsException;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
import org.elasticsearch.shield.license.LicenseService;
import java.io.File;
import java.nio.file.Path;
@ -29,9 +30,13 @@ public class ShieldPlugin extends AbstractPlugin {
public static final String NAME = "shield";
private final Settings settings;
private final boolean enabled;
private final boolean clientMode;
public ShieldPlugin(Settings settings) {
this.settings = settings;
this.enabled = settings.getAsBoolean("shield.enabled", true);
this.clientMode = clientMode(settings);
}
@Override
@ -49,8 +54,18 @@ public class ShieldPlugin extends AbstractPlugin {
return ImmutableList.<Class<? extends Module>>of(ShieldModule.class);
}
@Override
public Collection<Class<? extends LifecycleComponent>> services() {
return enabled && !clientMode ?
ImmutableList.<Class<? extends LifecycleComponent>>of(LicenseService.class) :
ImmutableList.<Class<? extends LifecycleComponent>>of();
}
@Override
public Settings additionalSettings() {
if (!enabled) {
return ImmutableSettings.EMPTY;
}
String setting = Headers.PREFIX + "." + UsernamePasswordToken.BASIC_AUTH_HEADER;
if (settings.get(setting) != null) {
return ImmutableSettings.EMPTY;
@ -77,4 +92,7 @@ public class ShieldPlugin extends AbstractPlugin {
return configDir(env).resolve(name);
}
public static boolean clientMode(Settings settings) {
return !"node".equals(settings.get(Client.CLIENT_TYPE_SETTING));
}
}

View File

@ -10,6 +10,7 @@ import org.elasticsearch.common.Nullable;
import org.elasticsearch.common.Strings;
import org.elasticsearch.common.io.stream.StreamInput;
import org.elasticsearch.common.io.stream.StreamOutput;
import org.elasticsearch.license.plugin.LicenseVersion;
import java.io.IOException;
import java.io.Serializable;
@ -24,7 +25,7 @@ public class ShieldVersion implements Serializable {
// the (internal) format of the id is there so we can easily do after/before checks on the id
public static final int V_1_0_0_ID = /*00*/1000099;
public static final ShieldVersion V_1_0_0 = new ShieldVersion(V_1_0_0_ID, true, Version.V_1_4_0);
public static final ShieldVersion V_1_0_0 = new ShieldVersion(V_1_0_0_ID, true, Version.V_1_4_0, LicenseVersion.V_1_0_0);
public static final ShieldVersion CURRENT = V_1_0_0;
@ -38,7 +39,7 @@ public class ShieldVersion implements Serializable {
return V_1_0_0;
default:
return new ShieldVersion(id, null, Version.CURRENT);
return new ShieldVersion(id, null, Version.CURRENT, LicenseVersion.CURRENT);
}
}
@ -97,8 +98,9 @@ public class ShieldVersion implements Serializable {
public final byte build;
public final Boolean snapshot;
public final Version minEsCompatibilityVersion;
public final LicenseVersion minLicenseCompatibilityVersion;
ShieldVersion(int id, @Nullable Boolean snapshot, Version minEsCompatibilityVersion) {
ShieldVersion(int id, @Nullable Boolean snapshot, Version minEsCompatibilityVersion, LicenseVersion minLicenseCompatibilityVersion) {
this.id = id;
this.major = (byte) ((id / 1000000) % 100);
this.minor = (byte) ((id / 10000) % 100);
@ -106,6 +108,7 @@ public class ShieldVersion implements Serializable {
this.build = (byte) (id % 100);
this.snapshot = snapshot;
this.minEsCompatibilityVersion = minEsCompatibilityVersion;
this.minLicenseCompatibilityVersion = minLicenseCompatibilityVersion;
}
public boolean snapshot() {
@ -154,6 +157,13 @@ public class ShieldVersion implements Serializable {
return minEsCompatibilityVersion;
}
/**
* @return The minimum license plugin version this shield version is compatible with.
*/
public LicenseVersion minimumLicenseCompatibilityVersion() {
return minLicenseCompatibilityVersion;
}
/**
* Just the version number (without -SNAPSHOT if snapshot).
*/

View File

@ -13,14 +13,20 @@ import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.action.search.SearchScrollRequest;
import org.elasticsearch.action.support.ActionFilter;
import org.elasticsearch.action.support.ActionFilterChain;
import org.elasticsearch.common.base.Predicate;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.license.plugin.core.LicenseExpiredException;
import org.elasticsearch.license.plugin.core.LicensesClientService;
import org.elasticsearch.shield.User;
import org.elasticsearch.shield.audit.AuditTrail;
import org.elasticsearch.shield.authc.AuthenticationService;
import org.elasticsearch.shield.authz.AuthorizationException;
import org.elasticsearch.shield.authz.AuthorizationService;
import org.elasticsearch.shield.signature.SignatureService;
import org.elasticsearch.shield.authz.Privilege;
import org.elasticsearch.shield.license.LicenseEventsNotifier;
import org.elasticsearch.shield.license.LicenseService;
import org.elasticsearch.shield.signature.SignatureException;
import org.elasticsearch.shield.signature.SignatureService;
import java.util.ArrayList;
import java.util.List;
@ -30,21 +36,45 @@ import java.util.List;
*/
public class ShieldActionFilter implements ActionFilter {
private static final Predicate<String> READ_ACTION_MATCHER = Privilege.Index.READ.predicate();
private final AuthenticationService authcService;
private final AuthorizationService authzService;
private final SignatureService signatureService;
private final AuditTrail auditTrail;
private volatile boolean licenseEnabled;
@Inject
public ShieldActionFilter(AuthenticationService authcService, AuthorizationService authzService, SignatureService signatureService, AuditTrail auditTrail) {
public ShieldActionFilter(AuthenticationService authcService, AuthorizationService authzService, SignatureService signatureService, AuditTrail auditTrail, LicenseEventsNotifier licenseEventsNotifier) {
this.authcService = authcService;
this.authzService = authzService;
this.signatureService = signatureService;
this.auditTrail = auditTrail;
licenseEventsNotifier.register(new LicensesClientService.Listener() {
@Override
public void onEnabled() {
licenseEnabled = true;
}
@Override
public void onDisabled() {
licenseEnabled = false;
}
});
}
@Override
public void apply(String action, ActionRequest request, ActionListener listener, ActionFilterChain chain) {
/**
A functional requirement - when the license of shield is disabled (invalid/expires), shield will continue
to operate normally, except all read operations will be blocked.
*/
if (!licenseEnabled && READ_ACTION_MATCHER.apply(action)) {
throw new LicenseExpiredException(LicenseService.FEATURE_NAME);
}
try {
/**
here we fallback on the system user. Internal system requests are requests that are triggered by

View File

@ -0,0 +1,43 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.license;
import org.elasticsearch.license.plugin.core.LicensesClientService;
import java.util.HashSet;
import java.util.Set;
/**
* Serves as a registry of license event listeners and enables notifying them about the
* different events.
*
* This class is required to serves as a bridge between the license service and any other
* service that needs to recieve license events. The reason for that is that some services
* that require such notifications also serves as a dependency for the licensing service
* which introdues a circular dependency in guice (e.g. TransportService). This class then
* serves as a bridge between the different services to eliminate such circular dependencies.
*/
public class LicenseEventsNotifier {
private final Set<LicensesClientService.Listener> listeners = new HashSet<>();
public void register(LicensesClientService.Listener listener) {
listeners.add(listener);
}
protected void notifyEnabled() {
for (LicensesClientService.Listener listener : listeners) {
listener.onEnabled();
}
}
protected void notifyDisabled() {
for (LicensesClientService.Listener listener : listeners) {
listener.onDisabled();
}
}
}

View File

@ -0,0 +1,44 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.license;
import org.elasticsearch.ElasticsearchIllegalStateException;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.license.plugin.LicenseVersion;
import org.elasticsearch.shield.ShieldVersion;
import org.elasticsearch.shield.support.AbstractShieldModule;
/**
*
*/
public class LicenseModule extends AbstractShieldModule.Node {
public LicenseModule(Settings settings) {
super(settings);
verifyLicensePlugin();
}
@Override
protected void configureNode() {
bind(LicenseService.class).asEagerSingleton();
bind(LicenseEventsNotifier.class).asEagerSingleton();
}
private void verifyLicensePlugin() {
try {
getClass().getClassLoader().loadClass("org.elasticsearch.license.plugin.LicensePlugin");
} catch (ClassNotFoundException cnfe) {
throw new ElasticsearchIllegalStateException("Shield plugin requires the elasticsearch-license plugin to be installed");
}
if (LicenseVersion.CURRENT.before(ShieldVersion.CURRENT.minLicenseCompatibilityVersion)) {
throw new ElasticsearchIllegalStateException("Shield [" + ShieldVersion.CURRENT +
"] requires minumum License plugin version [" + ShieldVersion.CURRENT.minLicenseCompatibilityVersion +
"], but installed License plugin version is [" + LicenseVersion.CURRENT + "]");
}
}
}

View File

@ -0,0 +1,74 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.shield.license;
import org.elasticsearch.ElasticsearchException;
import org.elasticsearch.common.component.AbstractLifecycleComponent;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.common.unit.TimeValue;
import org.elasticsearch.license.plugin.core.LicensesClientService;
import org.elasticsearch.license.plugin.core.LicensesService;
import org.elasticsearch.shield.ShieldPlugin;
/**
*
*/
public class LicenseService extends AbstractLifecycleComponent<LicenseService> {
public static final String FEATURE_NAME = ShieldPlugin.NAME;
private static final LicensesService.TrialLicenseOptions TRIAL_LICENSE_OPTIONS =
new LicensesService.TrialLicenseOptions(TimeValue.timeValueHours(30 * 24), 1000);
private final LicensesClientService licensesClientService;
private final LicenseEventsNotifier notifier;
private boolean enabled = false;
@Inject
public LicenseService(Settings settings, LicensesClientService licensesClientService, LicenseEventsNotifier notifier) {
super(settings);
this.licensesClientService = licensesClientService;
this.notifier = notifier;
}
public synchronized boolean enabled() {
return enabled;
}
@Override
protected void doStart() throws ElasticsearchException {
licensesClientService.register(FEATURE_NAME, TRIAL_LICENSE_OPTIONS, new InternalListener());
}
@Override
protected void doStop() throws ElasticsearchException {
}
@Override
protected void doClose() throws ElasticsearchException {
}
class InternalListener implements LicensesClientService.Listener {
@Override
public void onEnabled() {
synchronized (LicenseService.this) {
enabled = true;
notifier.notifyEnabled();
}
}
@Override
public void onDisabled() {
synchronized (LicenseService.this) {
enabled = false;
notifier.notifyDisabled();
}
}
}
}

View File

@ -0,0 +1,235 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
package org.elasticsearch.integration;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexResponse;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.Client;
import org.elasticsearch.cluster.ClusterChangedEvent;
import org.elasticsearch.cluster.ClusterService;
import org.elasticsearch.cluster.ClusterStateListener;
import org.elasticsearch.common.collect.ImmutableSet;
import org.elasticsearch.common.component.AbstractComponent;
import org.elasticsearch.common.inject.AbstractModule;
import org.elasticsearch.common.inject.Inject;
import org.elasticsearch.common.inject.Module;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.license.plugin.core.LicenseExpiredException;
import org.elasticsearch.license.plugin.core.LicensesClientService;
import org.elasticsearch.license.plugin.core.LicensesService;
import org.elasticsearch.plugins.AbstractPlugin;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.shield.license.LicenseService;
import org.elasticsearch.test.ElasticsearchIntegrationTest.ClusterScope;
import org.elasticsearch.test.ShieldIntegrationTest;
import org.elasticsearch.test.ShieldSettingsSource;
import org.junit.Test;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder;
import static org.elasticsearch.index.query.QueryBuilders.matchAllQuery;
import static org.elasticsearch.test.ElasticsearchIntegrationTest.Scope.SUITE;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertHitCount;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.is;
/**
*
*/
@ClusterScope(scope = SUITE)
public class LicensingTests extends ShieldIntegrationTest {
public static final String ROLES =
ShieldSettingsSource.DEFAULT_ROLE + ":\n" +
" cluster: all\n" +
" indices:\n" +
" '*': manage\n" +
" '/.*/': write\n" +
" 'test': read\n" +
" 'test1': read\n" +
"\n" +
"role_a:\n" +
" indices:\n" +
" 'a': all\n" +
"\n" +
"role_b:\n" +
" indices:\n" +
" 'b': all\n";
public static final String USERS =
ShieldSettingsSource.CONFIG_STANDARD_USER +
"user_a:{plain}passwd\n" +
"user_b:{plain}passwd\n";
public static final String USERS_ROLES =
ShieldSettingsSource.CONFIG_STANDARD_USER_ROLES +
"role_a:user_a,user_b\n" +
"role_b:user_b\n";
@Override
protected String configRoles() {
return ROLES;
}
@Override
protected String configUsers() {
return USERS;
}
@Override
protected String configUsersRoles() {
return USERS_ROLES;
}
@Override
protected Class<? extends Plugin> licensePluginClass() {
return InternalLicensePlugin.class;
}
@Override
protected String licensePluginName() {
return InternalLicensePlugin.NAME;
}
@Test
public void testEnableDisbleBehaviour() throws Exception {
IndexResponse indexResponse = index("test", "type", jsonBuilder()
.startObject()
.field("name", "value")
.endObject());
assertThat(indexResponse.isCreated(), is(true));
indexResponse = index("test1", "type", jsonBuilder()
.startObject()
.field("name", "value1")
.endObject());
assertThat(indexResponse.isCreated(), is(true));
refresh();
Client client = internalCluster().transportClient();
disableLicensing();
try {
client.prepareSearch().setQuery(matchAllQuery()).get();
fail("expected an license expired exception when running a search with disabled license");
} catch (LicenseExpiredException lee) {
// expected
assertThat(lee.feature(), equalTo(LicenseService.FEATURE_NAME));
}
try {
client.prepareGet("test1", "type", indexResponse.getId()).get();
fail("expected an license expired exception when running a get with disabled license");
} catch (LicenseExpiredException lee) {
// expected
assertThat(lee.feature(), equalTo(LicenseService.FEATURE_NAME));
}
enableLicensing();
SearchResponse searchResponse = client.prepareSearch().setQuery(matchAllQuery()).get();
assertNoFailures(searchResponse);
assertHitCount(searchResponse, 2);
GetResponse getResponse = client.prepareGet("test1", "type", indexResponse.getId()).get();
assertThat(getResponse.getId(), equalTo(indexResponse.getId()));
enableLicensing();
indexResponse = index("test", "type", jsonBuilder()
.startObject()
.field("name", "value2")
.endObject());
assertThat(indexResponse.isCreated(), is(true));
disableLicensing();
indexResponse = index("test", "type", jsonBuilder()
.startObject()
.field("name", "value3")
.endObject());
assertThat(indexResponse.isCreated(), is(true));
}
private void disableLicensing() {
for (InternalLicensesClientService service : internalCluster().getInstances(InternalLicensesClientService.class)) {
service.disable();
}
}
private void enableLicensing() {
for (InternalLicensesClientService service : internalCluster().getInstances(InternalLicensesClientService.class)) {
service.enable();
}
}
public static class InternalLicensePlugin extends AbstractPlugin {
static final String NAME = "internal-licensing";
@Override
public String name() {
return NAME;
}
@Override
public String description() {
return name();
}
@Override
public Collection<Class<? extends Module>> modules() {
return ImmutableSet.<Class<? extends Module>>of(InternalLicenseModule.class);
}
}
public static class InternalLicenseModule extends AbstractModule {
@Override
protected void configure() {
bind(InternalLicensesClientService.class).asEagerSingleton();
bind(LicensesClientService.class).to(InternalLicensesClientService.class);
}
}
public static class InternalLicensesClientService extends AbstractComponent implements LicensesClientService {
private final List<Listener> listeners = new ArrayList<>();
@Inject
public InternalLicensesClientService(Settings settings, ClusterService clusterService) {
super(settings);
clusterService.add(new ClusterStateListener() {
@Override
public void clusterChanged(ClusterChangedEvent event) {
enable();
}
});
}
@Override
public void register(String feature, LicensesService.TrialLicenseOptions trialLicenseOptions, Listener listener) {
listeners.add(listener);
}
void enable() {
for (Listener listener : listeners) {
listener.onEnabled();
}
}
void disable() {
for (Listener listener : listeners) {
listener.onDisabled();
}
}
}
}

View File

@ -9,7 +9,11 @@ import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.elasticsearch.action.admin.cluster.node.info.NodeInfo;
import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse;
import org.elasticsearch.action.admin.cluster.node.info.PluginInfo;
import org.elasticsearch.common.base.Function;
import org.elasticsearch.common.collect.Collections2;
import org.elasticsearch.http.HttpServerTransport;
import org.elasticsearch.license.plugin.LicensePlugin;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.elasticsearch.shield.authc.support.UsernamePasswordToken;
import org.elasticsearch.test.ShieldIntegrationTest;
@ -33,7 +37,13 @@ public class ShieldPluginTests extends ShieldIntegrationTest {
NodesInfoResponse nodeInfos = internalCluster().transportClient().admin().cluster().prepareNodesInfo().get();
logger.info("--> Checking nodes info that shield plugin is loaded");
for (NodeInfo nodeInfo : nodeInfos.getNodes()) {
assertThat(nodeInfo.getPlugins().getInfos(), hasSize(1));
assertThat(nodeInfo.getPlugins().getInfos(), hasSize(2));
assertThat(Collections2.transform(nodeInfo.getPlugins().getInfos(), new Function<PluginInfo, String>() {
@Override
public String apply(PluginInfo pluginInfo) {
return pluginInfo.getName();
}
}), contains(ShieldPlugin.NAME, LicensePlugin.NAME));
assertThat(nodeInfo.getPlugins().getInfos().get(0).getName(), is(ShieldPlugin.NAME));
}

View File

@ -9,11 +9,13 @@ import org.elasticsearch.action.ActionListener;
import org.elasticsearch.action.ActionRequest;
import org.elasticsearch.action.search.SearchScrollRequest;
import org.elasticsearch.action.support.ActionFilterChain;
import org.elasticsearch.license.plugin.core.LicensesClientService;
import org.elasticsearch.shield.User;
import org.elasticsearch.shield.audit.AuditTrail;
import org.elasticsearch.shield.authc.AuthenticationService;
import org.elasticsearch.shield.authz.AuthorizationException;
import org.elasticsearch.shield.authz.AuthorizationService;
import org.elasticsearch.shield.license.LicenseEventsNotifier;
import org.elasticsearch.shield.signature.SignatureService;
import org.elasticsearch.shield.signature.SignatureException;
import org.elasticsearch.test.ElasticsearchTestCase;
@ -34,6 +36,7 @@ public class ShieldActionFilterTests extends ElasticsearchTestCase {
private AuthorizationService authzService;
private SignatureService signatureService;
private AuditTrail auditTrail;
private LicenseEventsNotifier licenseEventsNotifier;
private ShieldActionFilter filter;
@Before
@ -42,7 +45,8 @@ public class ShieldActionFilterTests extends ElasticsearchTestCase {
authzService = mock(AuthorizationService.class);
signatureService = mock(SignatureService.class);
auditTrail = mock(AuditTrail.class);
filter = new ShieldActionFilter(authcService, authzService, signatureService, auditTrail);
licenseEventsNotifier = new MockLicenseEventsNotifier();
filter = new ShieldActionFilter(authcService, authzService, signatureService, auditTrail, licenseEventsNotifier);
}
@Test
@ -102,4 +106,11 @@ public class ShieldActionFilterTests extends ElasticsearchTestCase {
verify(auditTrail).tamperedRequest(user, "_action", request);
verifyNoMoreInteractions(chain);
}
private class MockLicenseEventsNotifier extends LicenseEventsNotifier {
@Override
public void register(LicensesClientService.Listener listener) {
listener.onEnabled();
}
}
}

View File

@ -11,9 +11,14 @@ import org.elasticsearch.action.admin.cluster.health.ClusterHealthResponse;
import org.elasticsearch.action.admin.cluster.health.ClusterHealthStatus;
import org.elasticsearch.action.admin.cluster.node.info.NodeInfo;
import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse;
import org.elasticsearch.action.admin.cluster.node.info.PluginInfo;
import org.elasticsearch.client.Client;
import org.elasticsearch.common.base.Function;
import org.elasticsearch.common.collect.Collections2;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.license.plugin.LicensePlugin;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.shield.ShieldPlugin;
import org.elasticsearch.shield.authc.support.SecuredString;
import org.junit.Before;
@ -25,8 +30,9 @@ import org.junit.rules.ExternalResource;
import java.io.File;
import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoTimeout;
import static org.hamcrest.CoreMatchers.equalTo;
import static org.hamcrest.CoreMatchers.is;
import static org.hamcrest.Matchers.contains;
import static org.hamcrest.Matchers.hasSize;
/**
* Base class to run tests against a cluster with shield installed.
@ -120,8 +126,13 @@ public abstract class ShieldIntegrationTest extends ElasticsearchIntegrationTest
public void assertShieldIsInstalled() {
NodesInfoResponse nodeInfos = client().admin().cluster().prepareNodesInfo().clear().setPlugins(true).get();
for (NodeInfo nodeInfo : nodeInfos) {
assertThat(ShieldPlugin.NAME + " should be the only installed plugin, found the following ones: " + nodeInfo.getPlugins().getInfos(), nodeInfo.getPlugins().getInfos().size(), equalTo(1));
assertThat(ShieldPlugin.NAME + " should be the only installed plugin, found the following ones: " + nodeInfo.getPlugins().getInfos(), nodeInfo.getPlugins().getInfos().get(0).getName(), equalTo(ShieldPlugin.NAME));
assertThat(nodeInfo.getPlugins().getInfos(), hasSize(2));
assertThat(Collections2.transform(nodeInfo.getPlugins().getInfos(), new Function<PluginInfo, String>() {
@Override
public String apply(PluginInfo pluginInfo) {
return pluginInfo.getName();
}
}), contains(ShieldPlugin.NAME, licensePluginName()));
}
}
@ -203,6 +214,14 @@ public abstract class ShieldIntegrationTest extends ElasticsearchIntegrationTest
return randomBoolean();
}
protected Class<? extends Plugin> licensePluginClass() {
return SHIELD_DEFAULT_SETTINGS.licensePluginClass();
}
protected String licensePluginName() {
return SHIELD_DEFAULT_SETTINGS.licensePluginName();
}
private class CustomShieldSettingsSource extends ShieldSettingsSource {
private CustomShieldSettingsSource(boolean sslTransportEnabled, File configDir, Scope scope) {
super(maxNumberOfNodes(), sslTransportEnabled, configDir, scope);
@ -243,6 +262,16 @@ public abstract class ShieldIntegrationTest extends ElasticsearchIntegrationTest
protected SecuredString transportClientPassword() {
return ShieldIntegrationTest.this.transportClientPassword();
}
@Override
protected Class<? extends Plugin> licensePluginClass() {
return ShieldIntegrationTest.this.licensePluginClass();
}
@Override
protected String licensePluginName() {
return ShieldIntegrationTest.this.licensePluginName();
}
}
protected void assertGreenClusterState(Client client) {

View File

@ -14,6 +14,8 @@ import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.os.OsUtils;
import org.elasticsearch.common.settings.ImmutableSettings;
import org.elasticsearch.common.settings.Settings;
import org.elasticsearch.license.plugin.LicensePlugin;
import org.elasticsearch.plugins.Plugin;
import org.elasticsearch.shield.ShieldPlugin;
import org.elasticsearch.shield.authc.esusers.ESUsersRealm;
import org.elasticsearch.shield.authc.support.SecuredString;
@ -77,7 +79,6 @@ public class ShieldSettingsSource extends ClusterDiscoveryConfiguration.UnicastZ
public ShieldSettingsSource(int numOfNodes, boolean sslTransportEnabled, File parentFolder, ElasticsearchIntegrationTest.Scope scope) {
super(numOfNodes, ImmutableSettings.builder()
.put("node.mode", "network")
.put("plugin.types", ShieldPlugin.class.getName())
.put("plugins.load_classpath_plugins", false)
.build(),
scope);
@ -91,6 +92,7 @@ public class ShieldSettingsSource extends ClusterDiscoveryConfiguration.UnicastZ
public Settings node(int nodeOrdinal) {
File folder = createFolder(parentFolder, subfolderPrefix + "-" + nodeOrdinal);
ImmutableSettings.Builder builder = ImmutableSettings.builder().put(super.node(nodeOrdinal))
.put("plugin.types", ShieldPlugin.class.getName() + "," + licensePluginClass().getName())
.put("shield.audit.enabled", RandomizedTest.randomBoolean())
.put(InternalSignatureService.FILE_SETTING, writeFile(folder, "system_key", systemKey))
.put("shield.authc.realms.esusers.type", ESUsersRealm.TYPE)
@ -110,6 +112,16 @@ public class ShieldSettingsSource extends ClusterDiscoveryConfiguration.UnicastZ
return builder.build();
}
@Override
public Settings transportClient() {
ImmutableSettings.Builder builder = ImmutableSettings.builder().put(super.transportClient())
.put("plugin.types", ShieldPlugin.class.getName())
.put(getClientSSLSettings());
setUser(builder, transportClientUsername(), transportClientPassword());
return builder.build();
}
protected String configUsers() {
return CONFIG_STANDARD_USER;
}
@ -138,12 +150,12 @@ public class ShieldSettingsSource extends ClusterDiscoveryConfiguration.UnicastZ
return new SecuredString(DEFAULT_PASSWORD.toCharArray());
}
@Override
public Settings transportClient() {
ImmutableSettings.Builder builder = ImmutableSettings.builder().put(super.transportClient())
.put(getClientSSLSettings());
setUser(builder, transportClientUsername(), transportClientPassword());
return builder.build();
protected Class<? extends Plugin> licensePluginClass() {
return LicensePlugin.class;
}
protected String licensePluginName() {
return LicensePlugin.NAME;
}
private void setUser(ImmutableSettings.Builder builder, String username, SecuredString password) {

View File

@ -12,6 +12,7 @@ import org.elasticsearch.action.Action;
import org.elasticsearch.common.io.Streams;
import org.elasticsearch.common.util.Callback;
import org.elasticsearch.test.ShieldIntegrationTest;
import org.elasticsearch.license.plugin.LicensePlugin;
import org.junit.BeforeClass;
import org.junit.Test;
@ -28,18 +29,18 @@ public class KnownActionsTests extends ShieldIntegrationTest {
private static ImmutableSet<String> knownActions;
private static ImmutableSet<String> knownHandlers;
private static ImmutableSet<String> coreActions;
private static ImmutableSet<String> externalActions;
@BeforeClass
public static void init() throws Exception {
knownActions = loadKnownActions();
knownHandlers = loadKnownHandlers();
coreActions = loadCoreActions();
externalActions = loadExternalActions();
}
@Test
public void testAllCoreTransportHandlersAreKnown() {
TransportService transportService = internalCluster().getInstance(TransportService.class);
public void testAllExternalTransportHandlersAreKnown() {
TransportService transportService = internalCluster().getDataNodeInstance(TransportService.class);
for (String handler : transportService.serverHandlers.keySet()) {
if (!knownActions.contains(handler)) {
assertThat("elasticsearch core transport handler [" + handler + "] is unknown to shield", knownHandlers, hasItem(handler));
@ -48,8 +49,8 @@ public class KnownActionsTests extends ShieldIntegrationTest {
}
@Test
public void testAllCoreActionsAreKnown() throws Exception {
for (String action : coreActions) {
public void testAllExternalActionsAreKnown() throws Exception {
for (String action : externalActions) {
assertThat("elasticsearch core action [" + action + "] is unknown to shield", knownActions, hasItem(action));
}
}
@ -57,13 +58,13 @@ public class KnownActionsTests extends ShieldIntegrationTest {
@Test
public void testAllKnownActionsAreValid() {
for (String knownAction : knownActions) {
assertThat("shield known action [" + knownAction + "] is unknown to core", coreActions, hasItems(knownAction));
assertThat("shield known action [" + knownAction + "] is unknown to core", externalActions, hasItems(knownAction));
}
}
@Test
public void testAllKnownTransportHandlersAreValid() {
TransportService transportService = internalCluster().getInstance(TransportService.class);
TransportService transportService = internalCluster().getDataNodeInstance(TransportService.class);
for (String knownHandler : knownHandlers) {
assertThat("shield known action [" + knownHandler + "] is unknown to core", transportService.serverHandlers.keySet(), hasItems(knownHandler));
}
@ -99,10 +100,22 @@ public class KnownActionsTests extends ShieldIntegrationTest {
return knownHandlersBuilder.build();
}
private static ImmutableSet<String> loadCoreActions() throws IOException, IllegalAccessException {
ImmutableSet.Builder<String> coreActionsBuilder = ImmutableSet.builder();
private static ImmutableSet<String> loadExternalActions() throws IOException, IllegalAccessException {
ImmutableSet.Builder<String> actions = ImmutableSet.builder();
// loading es core actions
ClassPath classPath = ClassPath.from(Action.class.getClassLoader());
ImmutableSet<ClassPath.ClassInfo> infos = classPath.getTopLevelClassesRecursive(Action.class.getPackage().getName());
loadActions(classPath, Action.class.getPackage().getName(), actions);
// also loading all actions from the licensing plugin
classPath = ClassPath.from(LicensePlugin.class.getClassLoader());
loadActions(classPath, LicensePlugin.class.getPackage().getName(), actions);
return actions.build();
}
private static void loadActions(ClassPath classPath, String packageName, ImmutableSet.Builder<String> actions) throws IOException, IllegalAccessException {
ImmutableSet<ClassPath.ClassInfo> infos = classPath.getTopLevelClassesRecursive(packageName);
for (ClassPath.ClassInfo info : infos) {
Class clazz = info.load();
if (Action.class.isAssignableFrom(clazz)) {
@ -115,10 +128,9 @@ public class KnownActionsTests extends ShieldIntegrationTest {
}
assertThat("every action should have a static field called INSTANCE, present but not static in " + clazz.getName(),
Modifier.isStatic(field.getModifiers()), is(true));
coreActionsBuilder.add(((Action) field.get(null)).name());
actions.add(((Action) field.get(null)).name());
}
}
}
return coreActionsBuilder.build();
}
}

View File

@ -73,4 +73,7 @@ indices:data/write/delete/by_query
indices:data/write/index
indices:data/write/script/delete
indices:data/write/script/put
indices:data/write/update
indices:data/write/update
cluster:admin/plugin/license/get
cluster:admin/plugin/license/delete
cluster:admin/plugin/license/put

View File

@ -83,3 +83,4 @@ internal:index/shard/recovery/start_recovery
internal:index/shard/recovery/translog_ops
internal:river/state/publish
internal:admin/repository/verify
internal:plugin/license/cluster/register_trial_license

View File

@ -35,6 +35,7 @@ grant {
permission java.security.SecurityPermission "putProviderProperty.BC";
permission java.security.SecurityPermission "insertProvider.BC";
permission java.security.SecurityPermission "getProperty.ssl.KeyManagerFactory.algorithm";
permission java.security.SecurityPermission "getProperty.ssl.TrustManagerFactory.algorithm";
//this shouldn't be in a production environment, just to run tests:
permission java.lang.reflect.ReflectPermission "suppressAccessChecks";