From e646fd5edc12cfaf2db8893461382e0d858bb74b Mon Sep 17 00:00:00 2001 From: uboness Date: Fri, 14 Nov 2014 14:34:30 -0800 Subject: [PATCH] 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@6ba7a10cd4a8c662e12f77714e31137eef356226 --- pom.xml | 23 +- .../elasticsearch/shield/ShieldModule.java | 11 +- .../elasticsearch/shield/ShieldPlugin.java | 22 +- .../elasticsearch/shield/ShieldVersion.java | 16 +- .../shield/action/ShieldActionFilter.java | 34 ++- .../shield/license/LicenseEventsNotifier.java | 43 ++++ .../shield/license/LicenseModule.java | 44 ++++ .../shield/license/LicenseService.java | 74 ++++++ .../integration/LicensingTests.java | 235 ++++++++++++++++++ .../shield/ShieldPluginTests.java | 12 +- .../action/ShieldActionFilterTests.java | 13 +- .../test/ShieldIntegrationTest.java | 35 ++- .../test/ShieldSettingsSource.java | 26 +- .../transport/KnownActionsTests.java | 38 ++- .../org/elasticsearch/transport/actions | 5 +- .../org/elasticsearch/transport/handlers | 1 + tests.policy | 1 + 17 files changed, 590 insertions(+), 43 deletions(-) create mode 100644 src/main/java/org/elasticsearch/shield/license/LicenseEventsNotifier.java create mode 100644 src/main/java/org/elasticsearch/shield/license/LicenseModule.java create mode 100644 src/main/java/org/elasticsearch/shield/license/LicenseService.java create mode 100644 src/test/java/org/elasticsearch/integration/LicensingTests.java diff --git a/pom.xml b/pom.xml index 3fcd4292131..0f71b185c75 100644 --- a/pom.xml +++ b/pom.xml @@ -17,8 +17,19 @@ - oss.sonatype.org-snapshot - http://oss.sonatype.org/content/repositories/snapshots + es-releases + http://maven.elasticsearch.org/libs-release-local + + true + daily + + + false + + + + es-snapshots + http://maven.elasticsearch.org/libs-snapshot false @@ -33,6 +44,7 @@ 4.10.2 4.10.2 1.4.0 + 1.0.0-beta1 auto true @@ -150,6 +162,13 @@ 1.9 + + org.elasticsearch + elasticsearch-license-plugin + ${license.plugin.version} + provided + + diff --git a/src/main/java/org/elasticsearch/shield/ShieldModule.java b/src/main/java/org/elasticsearch/shield/ShieldModule.java index 04df2eb6228..dbf6bfbc46b 100644 --- a/src/main/java/org/elasticsearch/shield/ShieldModule.java +++ b/src/main/java/org/elasticsearch/shield/ShieldModule.java @@ -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 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.of( @@ -45,6 +39,7 @@ public class ShieldModule extends AbstractShieldModule.Spawn { } return ImmutableList.of( + new LicenseModule(settings), new AuthenticationModule(settings), new AuthorizationModule(settings), new AuditTrailModule(settings), diff --git a/src/main/java/org/elasticsearch/shield/ShieldPlugin.java b/src/main/java/org/elasticsearch/shield/ShieldPlugin.java index 898ee1c7a6e..3195b12034b 100644 --- a/src/main/java/org/elasticsearch/shield/ShieldPlugin.java +++ b/src/main/java/org/elasticsearch/shield/ShieldPlugin.java @@ -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.>of(ShieldModule.class); } + @Override + public Collection> services() { + return enabled && !clientMode ? + ImmutableList.>of(LicenseService.class) : + ImmutableList.>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)); + } } diff --git a/src/main/java/org/elasticsearch/shield/ShieldVersion.java b/src/main/java/org/elasticsearch/shield/ShieldVersion.java index 2507381f840..a1a0341b799 100644 --- a/src/main/java/org/elasticsearch/shield/ShieldVersion.java +++ b/src/main/java/org/elasticsearch/shield/ShieldVersion.java @@ -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). */ diff --git a/src/main/java/org/elasticsearch/shield/action/ShieldActionFilter.java b/src/main/java/org/elasticsearch/shield/action/ShieldActionFilter.java index d72a3b7c86e..a4b243ba7b6 100644 --- a/src/main/java/org/elasticsearch/shield/action/ShieldActionFilter.java +++ b/src/main/java/org/elasticsearch/shield/action/ShieldActionFilter.java @@ -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 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 diff --git a/src/main/java/org/elasticsearch/shield/license/LicenseEventsNotifier.java b/src/main/java/org/elasticsearch/shield/license/LicenseEventsNotifier.java new file mode 100644 index 00000000000..bbcc18d55a1 --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/license/LicenseEventsNotifier.java @@ -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 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(); + } + } + +} diff --git a/src/main/java/org/elasticsearch/shield/license/LicenseModule.java b/src/main/java/org/elasticsearch/shield/license/LicenseModule.java new file mode 100644 index 00000000000..bfb4babb75d --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/license/LicenseModule.java @@ -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 + "]"); + } + } + +} \ No newline at end of file diff --git a/src/main/java/org/elasticsearch/shield/license/LicenseService.java b/src/main/java/org/elasticsearch/shield/license/LicenseService.java new file mode 100644 index 00000000000..ba3fe09ea63 --- /dev/null +++ b/src/main/java/org/elasticsearch/shield/license/LicenseService.java @@ -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 { + + 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(); + } + } + } +} diff --git a/src/test/java/org/elasticsearch/integration/LicensingTests.java b/src/test/java/org/elasticsearch/integration/LicensingTests.java new file mode 100644 index 00000000000..05e34d81a34 --- /dev/null +++ b/src/test/java/org/elasticsearch/integration/LicensingTests.java @@ -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 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> modules() { + return ImmutableSet.>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 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(); + } + } + } +} diff --git a/src/test/java/org/elasticsearch/shield/ShieldPluginTests.java b/src/test/java/org/elasticsearch/shield/ShieldPluginTests.java index cac7e949a01..b15f5bcfd6c 100644 --- a/src/test/java/org/elasticsearch/shield/ShieldPluginTests.java +++ b/src/test/java/org/elasticsearch/shield/ShieldPluginTests.java @@ -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() { + @Override + public String apply(PluginInfo pluginInfo) { + return pluginInfo.getName(); + } + }), contains(ShieldPlugin.NAME, LicensePlugin.NAME)); assertThat(nodeInfo.getPlugins().getInfos().get(0).getName(), is(ShieldPlugin.NAME)); } diff --git a/src/test/java/org/elasticsearch/shield/action/ShieldActionFilterTests.java b/src/test/java/org/elasticsearch/shield/action/ShieldActionFilterTests.java index d450742fb48..136d06a4842 100644 --- a/src/test/java/org/elasticsearch/shield/action/ShieldActionFilterTests.java +++ b/src/test/java/org/elasticsearch/shield/action/ShieldActionFilterTests.java @@ -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(); + } + } } diff --git a/src/test/java/org/elasticsearch/test/ShieldIntegrationTest.java b/src/test/java/org/elasticsearch/test/ShieldIntegrationTest.java index 11790d338fc..f2bec1d087a 100644 --- a/src/test/java/org/elasticsearch/test/ShieldIntegrationTest.java +++ b/src/test/java/org/elasticsearch/test/ShieldIntegrationTest.java @@ -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() { + @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 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 licensePluginClass() { + return ShieldIntegrationTest.this.licensePluginClass(); + } + + @Override + protected String licensePluginName() { + return ShieldIntegrationTest.this.licensePluginName(); + } } protected void assertGreenClusterState(Client client) { diff --git a/src/test/java/org/elasticsearch/test/ShieldSettingsSource.java b/src/test/java/org/elasticsearch/test/ShieldSettingsSource.java index 2334042fa37..578aff72e0f 100644 --- a/src/test/java/org/elasticsearch/test/ShieldSettingsSource.java +++ b/src/test/java/org/elasticsearch/test/ShieldSettingsSource.java @@ -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 licensePluginClass() { + return LicensePlugin.class; + } + + protected String licensePluginName() { + return LicensePlugin.NAME; } private void setUser(ImmutableSettings.Builder builder, String username, SecuredString password) { diff --git a/src/test/java/org/elasticsearch/transport/KnownActionsTests.java b/src/test/java/org/elasticsearch/transport/KnownActionsTests.java index 92f25affb71..5ba33ba85ad 100644 --- a/src/test/java/org/elasticsearch/transport/KnownActionsTests.java +++ b/src/test/java/org/elasticsearch/transport/KnownActionsTests.java @@ -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 knownActions; private static ImmutableSet knownHandlers; - private static ImmutableSet coreActions; + private static ImmutableSet 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 loadCoreActions() throws IOException, IllegalAccessException { - ImmutableSet.Builder coreActionsBuilder = ImmutableSet.builder(); + private static ImmutableSet loadExternalActions() throws IOException, IllegalAccessException { + ImmutableSet.Builder actions = ImmutableSet.builder(); + + // loading es core actions ClassPath classPath = ClassPath.from(Action.class.getClassLoader()); - ImmutableSet 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 actions) throws IOException, IllegalAccessException { + ImmutableSet 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(); } } diff --git a/src/test/resources/org/elasticsearch/transport/actions b/src/test/resources/org/elasticsearch/transport/actions index d7bf93136ef..1bfd5d0891e 100644 --- a/src/test/resources/org/elasticsearch/transport/actions +++ b/src/test/resources/org/elasticsearch/transport/actions @@ -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 \ No newline at end of file +indices:data/write/update +cluster:admin/plugin/license/get +cluster:admin/plugin/license/delete +cluster:admin/plugin/license/put \ No newline at end of file diff --git a/src/test/resources/org/elasticsearch/transport/handlers b/src/test/resources/org/elasticsearch/transport/handlers index 0d3ac2ba143..325f8817f1e 100644 --- a/src/test/resources/org/elasticsearch/transport/handlers +++ b/src/test/resources/org/elasticsearch/transport/handlers @@ -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 diff --git a/tests.policy b/tests.policy index ad06580d8b8..010d9c894eb 100644 --- a/tests.policy +++ b/tests.policy @@ -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";