diff --git a/marvel/src/main/java/org/elasticsearch/marvel/license/MarvelLicensee.java b/marvel/src/main/java/org/elasticsearch/marvel/license/MarvelLicensee.java index 8cfb3c7ab41..c48611949a7 100644 --- a/marvel/src/main/java/org/elasticsearch/marvel/license/MarvelLicensee.java +++ b/marvel/src/main/java/org/elasticsearch/marvel/license/MarvelLicensee.java @@ -52,6 +52,9 @@ public class MarvelLicensee extends AbstractLicenseeComponent im } public boolean collectionEnabled() { + // when checking multiple parts of the status, we should get a local reference to the status object since it is + // volatile and can change between check statements... + Status status = this.status; return status.getMode() != License.OperationMode.NONE && status.getLicenseState() != LicenseState.DISABLED; } diff --git a/shield/src/main/java/org/elasticsearch/shield/ShieldDisabledModule.java b/shield/src/main/java/org/elasticsearch/shield/ShieldDisabledModule.java index 2d1f1f2f482..0bd5d528aa9 100644 --- a/shield/src/main/java/org/elasticsearch/shield/ShieldDisabledModule.java +++ b/shield/src/main/java/org/elasticsearch/shield/ShieldDisabledModule.java @@ -7,7 +7,7 @@ package org.elasticsearch.shield; import org.elasticsearch.common.inject.util.Providers; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.shield.license.LicenseService; +import org.elasticsearch.shield.license.ShieldLicenseState; import org.elasticsearch.shield.support.AbstractShieldModule; public class ShieldDisabledModule extends AbstractShieldModule { @@ -21,7 +21,7 @@ public class ShieldDisabledModule extends AbstractShieldModule { assert !shieldEnabled : "shield disabled module should only get loaded with shield disabled"; if (!clientMode) { // required by the shield info rest action (when shield is disabled) - bind(LicenseService.class).toProvider(Providers.of(null)); + bind(ShieldLicenseState.class).toProvider(Providers.of(null)); } } } diff --git a/shield/src/main/java/org/elasticsearch/shield/ShieldPlugin.java b/shield/src/main/java/org/elasticsearch/shield/ShieldPlugin.java index d639da0b864..e26df0606e0 100644 --- a/shield/src/main/java/org/elasticsearch/shield/ShieldPlugin.java +++ b/shield/src/main/java/org/elasticsearch/shield/ShieldPlugin.java @@ -32,12 +32,11 @@ import org.elasticsearch.shield.authc.support.UsernamePasswordToken; import org.elasticsearch.shield.authz.AuthorizationModule; import org.elasticsearch.index.SearcherWrapperInstaller; import org.elasticsearch.shield.authz.accesscontrol.OptOutQueryCache; -import org.elasticsearch.shield.authz.accesscontrol.ShieldIndexSearcherWrapper; import org.elasticsearch.shield.authz.store.FileRolesStore; import org.elasticsearch.shield.crypto.CryptoModule; import org.elasticsearch.shield.crypto.InternalCryptoService; import org.elasticsearch.shield.license.LicenseModule; -import org.elasticsearch.shield.license.LicenseService; +import org.elasticsearch.shield.license.ShieldLicensee; import org.elasticsearch.shield.rest.ShieldRestModule; import org.elasticsearch.shield.rest.action.RestShieldInfoAction; import org.elasticsearch.shield.rest.action.authc.cache.RestClearRealmCacheAction; @@ -125,7 +124,7 @@ public class ShieldPlugin extends Plugin { @Override public Collection> nodeServices() { if (enabled && clientMode == false) { - return Arrays.>asList(LicenseService.class, InternalCryptoService.class, FileRolesStore.class, Realms.class, IPFilter.class); + return Arrays.>asList(ShieldLicensee.class, InternalCryptoService.class, FileRolesStore.class, Realms.class, IPFilter.class); } return Collections.emptyList(); } diff --git a/shield/src/main/java/org/elasticsearch/shield/action/ShieldActionFilter.java b/shield/src/main/java/org/elasticsearch/shield/action/ShieldActionFilter.java index 69aff42244f..8ecab39b7c8 100644 --- a/shield/src/main/java/org/elasticsearch/shield/action/ShieldActionFilter.java +++ b/shield/src/main/java/org/elasticsearch/shield/action/ShieldActionFilter.java @@ -16,8 +16,8 @@ import org.elasticsearch.action.support.ActionFilterChain; import org.elasticsearch.common.component.AbstractComponent; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.license.plugin.core.LicenseState; import org.elasticsearch.license.plugin.core.LicenseUtils; +import org.elasticsearch.shield.ShieldPlugin; import org.elasticsearch.shield.User; import org.elasticsearch.shield.action.interceptor.RequestInterceptor; import org.elasticsearch.shield.audit.AuditTrail; @@ -25,8 +25,7 @@ import org.elasticsearch.shield.authc.AuthenticationService; import org.elasticsearch.shield.authz.AuthorizationService; import org.elasticsearch.shield.authz.Privilege; import org.elasticsearch.shield.crypto.CryptoService; -import org.elasticsearch.shield.license.LicenseEventsNotifier; -import org.elasticsearch.shield.license.LicenseService; +import org.elasticsearch.shield.license.ShieldLicenseState; import java.io.IOException; import java.util.ArrayList; @@ -49,24 +48,18 @@ public class ShieldActionFilter extends AbstractComponent implements ActionFilte private final AuditTrail auditTrail; private final ShieldActionMapper actionMapper; private final Set requestInterceptors; - - private volatile boolean licenseEnabled = true; + private final ShieldLicenseState licenseState; @Inject public ShieldActionFilter(Settings settings, AuthenticationService authcService, AuthorizationService authzService, CryptoService cryptoService, - AuditTrail auditTrail, LicenseEventsNotifier licenseEventsNotifier, ShieldActionMapper actionMapper, Set requestInterceptors) { + AuditTrail auditTrail, ShieldLicenseState licenseState, ShieldActionMapper actionMapper, Set requestInterceptors) { super(settings); this.authcService = authcService; this.authzService = authzService; this.cryptoService = cryptoService; this.auditTrail = auditTrail; this.actionMapper = actionMapper; - licenseEventsNotifier.register(new LicenseEventsNotifier.Listener() { - @Override - public void notify(LicenseState state) { - licenseEnabled = state != LicenseState.DISABLED; - } - }); + this.licenseState = licenseState; this.requestInterceptors = requestInterceptors; } @@ -77,36 +70,40 @@ public class ShieldActionFilter extends AbstractComponent implements ActionFilte 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 && LICENSE_EXPIRATION_ACTION_MATCHER.test(action)) { + if (!licenseState.statsAndHealthEnabled() && LICENSE_EXPIRATION_ACTION_MATCHER.test(action)) { logger.error("blocking [{}] operation due to expired license. Cluster health, cluster stats and indices stats \n" + "operations are blocked on shield license expiration. All data operations (read and write) continue to work. \n" + "If you have a new license, please update it. Otherwise, please reach out to your support contact.", action); - throw LicenseUtils.newExpirationException(LicenseService.FEATURE_NAME); + throw LicenseUtils.newExpirationException(ShieldPlugin.NAME); } try { - /** - here we fallback on the system user. Internal system requests are requests that are triggered by - the system itself (e.g. pings, update mappings, share relocation, etc...) and were not originated - by user interaction. Since these requests are triggered by es core modules, they are security - agnostic and therefore not associated with any user. When these requests execute locally, they - are executed directly on their relevant action. Since there is no other way a request can make - it to the action without an associated user (not via REST or transport - this is taken care of by - the {@link Rest} filter and the {@link ServerTransport} filter respectively), it's safe to assume a system user - here if a request is not associated with any other user. - */ + if (licenseState.securityEnabled()) { + /** + here we fallback on the system user. Internal system requests are requests that are triggered by + the system itself (e.g. pings, update mappings, share relocation, etc...) and were not originated + by user interaction. Since these requests are triggered by es core modules, they are security + agnostic and therefore not associated with any user. When these requests execute locally, they + are executed directly on their relevant action. Since there is no other way a request can make + it to the action without an associated user (not via REST or transport - this is taken care of by + the {@link Rest} filter and the {@link ServerTransport} filter respectively), it's safe to assume a system user + here if a request is not associated with any other user. + */ - String shieldAction = actionMapper.action(action, request); - User user = authcService.authenticate(shieldAction, request, User.SYSTEM); - authzService.authorize(user, shieldAction, request); - request = unsign(user, shieldAction, request); + String shieldAction = actionMapper.action(action, request); + User user = authcService.authenticate(shieldAction, request, User.SYSTEM); + authzService.authorize(user, shieldAction, request); + request = unsign(user, shieldAction, request); - for (RequestInterceptor interceptor : requestInterceptors) { - if (interceptor.supports(request)) { - interceptor.intercept(request, user); + for (RequestInterceptor interceptor : requestInterceptors) { + if (interceptor.supports(request)) { + interceptor.intercept(request, user); + } } + chain.proceed(action, request, new SigningListener(this, listener)); + } else { + chain.proceed(action, request, listener); } - chain.proceed(action, request, new SigningListener(this, listener)); } catch (Throwable t) { listener.onFailure(t); } diff --git a/shield/src/main/java/org/elasticsearch/shield/authz/accesscontrol/ShieldIndexSearcherWrapper.java b/shield/src/main/java/org/elasticsearch/shield/authz/accesscontrol/ShieldIndexSearcherWrapper.java index 9b9298b75c4..52afb3298d2 100644 --- a/shield/src/main/java/org/elasticsearch/shield/authz/accesscontrol/ShieldIndexSearcherWrapper.java +++ b/shield/src/main/java/org/elasticsearch/shield/authz/accesscontrol/ShieldIndexSearcherWrapper.java @@ -30,6 +30,7 @@ import org.elasticsearch.index.shard.ShardId; import org.elasticsearch.index.shard.ShardUtils; import org.elasticsearch.shield.authz.InternalAuthorizationService; import org.elasticsearch.shield.authz.accesscontrol.DocumentSubsetReader.DocumentSubsetDirectoryReader; +import org.elasticsearch.shield.license.ShieldLicenseState; import org.elasticsearch.shield.support.Exceptions; import java.io.IOException; @@ -54,13 +55,16 @@ public final class ShieldIndexSearcherWrapper extends AbstractComponent implemen private final Set allowedMetaFields; private final IndexQueryParserService parserService; private final BitsetFilterCache bitsetFilterCache; + private final ShieldLicenseState shieldLicenseState; @Inject - public ShieldIndexSearcherWrapper(@IndexSettings Settings indexSettings, IndexQueryParserService parserService, MapperService mapperService, BitsetFilterCache bitsetFilterCache) { + public ShieldIndexSearcherWrapper(@IndexSettings Settings indexSettings, IndexQueryParserService parserService, + MapperService mapperService, BitsetFilterCache bitsetFilterCache, ShieldLicenseState shieldLicenseState) { super(indexSettings); this.mapperService = mapperService; this.parserService = parserService; this.bitsetFilterCache = bitsetFilterCache; + this.shieldLicenseState = shieldLicenseState; Set allowedMetaFields = new HashSet<>(); allowedMetaFields.addAll(Arrays.asList(MapperService.getAllMetaFields())); @@ -73,6 +77,10 @@ public final class ShieldIndexSearcherWrapper extends AbstractComponent implemen @Override public DirectoryReader wrap(DirectoryReader reader) { + if (shieldLicenseState.documentAndFieldLevelSecurityEnabled() == false) { + return reader; + } + final Set allowedMetaFields = this.allowedMetaFields; try { RequestContext context = RequestContext.current(); @@ -124,6 +132,10 @@ public final class ShieldIndexSearcherWrapper extends AbstractComponent implemen @Override public IndexSearcher wrap(EngineConfig engineConfig, IndexSearcher searcher) throws EngineException { + if (shieldLicenseState.documentAndFieldLevelSecurityEnabled() == false) { + return searcher; + } + final DirectoryReader directoryReader = (DirectoryReader) searcher.getIndexReader(); if (directoryReader instanceof DocumentSubsetDirectoryReader) { // The reasons why we return a custom searcher: diff --git a/shield/src/main/java/org/elasticsearch/shield/license/LicenseEventsNotifier.java b/shield/src/main/java/org/elasticsearch/shield/license/LicenseEventsNotifier.java deleted file mode 100644 index 1ea9f42b10a..00000000000 --- a/shield/src/main/java/org/elasticsearch/shield/license/LicenseEventsNotifier.java +++ /dev/null @@ -1,41 +0,0 @@ -/* - * 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.LicenseState; - -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(Listener listener) { - listeners.add(listener); - } - - protected void notify(LicenseState state) { - for (Listener listener : listeners) { - listener.notify(state); - } - } - - public static interface Listener { - - void notify(LicenseState state); - } -} diff --git a/shield/src/main/java/org/elasticsearch/shield/license/LicenseModule.java b/shield/src/main/java/org/elasticsearch/shield/license/LicenseModule.java index 2f5b8a50728..9c7d015adb8 100644 --- a/shield/src/main/java/org/elasticsearch/shield/license/LicenseModule.java +++ b/shield/src/main/java/org/elasticsearch/shield/license/LicenseModule.java @@ -20,8 +20,8 @@ public class LicenseModule extends AbstractShieldModule.Node { @Override protected void configureNode() { - bind(LicenseService.class).asEagerSingleton(); - bind(LicenseEventsNotifier.class).asEagerSingleton(); + bind(ShieldLicensee.class).asEagerSingleton(); + bind(ShieldLicenseState.class).asEagerSingleton(); } private void verifyLicensePlugin() { diff --git a/shield/src/main/java/org/elasticsearch/shield/license/ShieldLicenseState.java b/shield/src/main/java/org/elasticsearch/shield/license/ShieldLicenseState.java new file mode 100644 index 00000000000..f811e8e206e --- /dev/null +++ b/shield/src/main/java/org/elasticsearch/shield/license/ShieldLicenseState.java @@ -0,0 +1,50 @@ +/* + * 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.core.License.OperationMode; +import org.elasticsearch.license.plugin.core.LicenseState; +import org.elasticsearch.license.plugin.core.Licensee.Status; + + +/** + * This class serves to decouple shield code that needs to check the license state from the {@link ShieldLicensee} as the + * tight coupling causes issues with guice injection and circular dependencies + */ +public class ShieldLicenseState { + + // if we start disabled then we can emit false disabled messages and block legitimate requests... + protected volatile Status status = new Status(OperationMode.TRIAL, LicenseState.ENABLED); + + /** + * @return true if the license allows for security features to be enabled (authc, authz, ip filter, audit, etc) + */ + public boolean securityEnabled() { + return status.getMode() != OperationMode.BASIC; + } + + /** + * Indicates whether the stats and health API calls should be allowed. If a license is expired and past the grace + * period then we deny these calls. + * + * @return true if the license allows for the stats and health apis to be used. + */ + public boolean statsAndHealthEnabled() { + return status.getLicenseState() != LicenseState.DISABLED; + } + + /** + * @return true if the license enables DLS and FLS + */ + public boolean documentAndFieldLevelSecurityEnabled() { + Status status = this.status; + return status.getMode() == OperationMode.PLATINUM || status.getMode() == OperationMode.TRIAL; + } + + void updateStatus(Status status) { + this.status = status; + } +} diff --git a/shield/src/main/java/org/elasticsearch/shield/license/LicenseService.java b/shield/src/main/java/org/elasticsearch/shield/license/ShieldLicensee.java similarity index 59% rename from shield/src/main/java/org/elasticsearch/shield/license/LicenseService.java rename to shield/src/main/java/org/elasticsearch/shield/license/ShieldLicensee.java index 95ae692bfda..82a48bd43a0 100644 --- a/shield/src/main/java/org/elasticsearch/shield/license/LicenseService.java +++ b/shield/src/main/java/org/elasticsearch/shield/license/ShieldLicensee.java @@ -7,37 +7,33 @@ package org.elasticsearch.shield.license; import org.elasticsearch.ElasticsearchException; import org.elasticsearch.common.Strings; -import org.elasticsearch.common.component.AbstractLifecycleComponent; import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.license.core.License; -import org.elasticsearch.license.plugin.core.LicenseState; -import org.elasticsearch.license.plugin.core.Licensee; -import org.elasticsearch.license.plugin.core.LicenseeRegistry; +import org.elasticsearch.license.core.License.OperationMode; +import org.elasticsearch.license.plugin.core.*; import org.elasticsearch.shield.ShieldPlugin; /** * */ -public class LicenseService extends AbstractLifecycleComponent implements Licensee { +public class ShieldLicensee extends AbstractLicenseeComponent implements Licensee { - public static final String FEATURE_NAME = ShieldPlugin.NAME; - - private final LicenseeRegistry licenseeRegistry; - private final LicenseEventsNotifier notifier; - - private volatile LicenseState state = LicenseState.DISABLED; + private final boolean isTribeNode; + private final ShieldLicenseState shieldLicenseState; @Inject - public LicenseService(Settings settings, LicenseeRegistry licenseeRegistry, LicenseEventsNotifier notifier) { - super(settings); - this.licenseeRegistry = licenseeRegistry; - this.notifier = notifier; - } - - @Override - public String id() { - return FEATURE_NAME; + public ShieldLicensee(Settings settings, LicenseeRegistry clientService, + LicensesManagerService managerService, ShieldLicenseState shieldLicenseState) { + super(settings, ShieldPlugin.NAME, clientService, managerService); + add(new Listener() { + @Override + public void onChange(License license, Status status) { + shieldLicenseState.updateStatus(status); + } + }); + this.shieldLicenseState = shieldLicenseState; + this.isTribeNode = settings.getGroups("tribe", true).isEmpty() == false; } @Override @@ -78,31 +74,13 @@ public class LicenseService extends AbstractLifecycleComponent i } @Override - public void onChange(License license, LicenseState state) { - synchronized (this) { - this.state = state; - notifier.notify(state); - } - } - public LicenseState state() { - return state; - } - - @Override - protected void doStart() throws ElasticsearchException { - if (settings.getGroups("tribe", true).isEmpty()) { - licenseeRegistry.register(this); - } else { + protected void doStart() throws ElasticsearchException {; + if (isTribeNode) { //TODO currently we disable licensing on tribe node. remove this once es core supports merging cluster - onChange(null, LicenseState.ENABLED); + this.status = new Status(OperationMode.TRIAL, LicenseState.ENABLED); + shieldLicenseState.updateStatus(status); + } else { + super.doStart(); } } - - @Override - protected void doStop() throws ElasticsearchException { - } - - @Override - protected void doClose() throws ElasticsearchException { - } } diff --git a/shield/src/main/java/org/elasticsearch/shield/rest/ShieldRestFilter.java b/shield/src/main/java/org/elasticsearch/shield/rest/ShieldRestFilter.java index 221def2fe18..dbab5d25ec2 100644 --- a/shield/src/main/java/org/elasticsearch/shield/rest/ShieldRestFilter.java +++ b/shield/src/main/java/org/elasticsearch/shield/rest/ShieldRestFilter.java @@ -13,6 +13,7 @@ import org.elasticsearch.http.netty.NettyHttpRequest; import org.elasticsearch.rest.*; import org.elasticsearch.shield.authc.AuthenticationService; import org.elasticsearch.shield.authc.pki.PkiRealm; +import org.elasticsearch.shield.license.ShieldLicenseState; import org.elasticsearch.shield.transport.SSLClientAuth; import org.elasticsearch.shield.transport.netty.ShieldNettyHttpServerTransport; import org.jboss.netty.handler.ssl.SslHandler; @@ -28,11 +29,13 @@ public class ShieldRestFilter extends RestFilter { private final AuthenticationService service; private final ESLogger logger; + private final ShieldLicenseState licenseState; private final boolean extractClientCertificate; @Inject - public ShieldRestFilter(AuthenticationService service, RestController controller, Settings settings) { + public ShieldRestFilter(AuthenticationService service, RestController controller, Settings settings, ShieldLicenseState licenseState) { this.service = service; + this.licenseState = licenseState; controller.registerFilter(this); boolean ssl = settings.getAsBoolean(ShieldNettyHttpServerTransport.HTTP_SSL_SETTING, ShieldNettyHttpServerTransport.HTTP_SSL_DEFAULT); extractClientCertificate = ssl && SSLClientAuth.parse(settings.get(ShieldNettyHttpServerTransport.HTTP_CLIENT_AUTH_SETTING), ShieldNettyHttpServerTransport.HTTP_CLIENT_AUTH_DEFAULT).enabled(); @@ -47,15 +50,17 @@ public class ShieldRestFilter extends RestFilter { @Override public void process(RestRequest request, RestChannel channel, RestFilterChain filterChain) throws Exception { - // CORS - allow for preflight unauthenticated OPTIONS request - if (request.method() != RestRequest.Method.OPTIONS) { - if (extractClientCertificate) { - putClientCertificateInContext(request, logger); + if (licenseState.securityEnabled()) { + // CORS - allow for preflight unauthenticated OPTIONS request + if (request.method() != RestRequest.Method.OPTIONS) { + if (extractClientCertificate) { + putClientCertificateInContext(request, logger); + } + service.authenticate(request); } - service.authenticate(request); - } - RemoteHostHeader.process(request); + RemoteHostHeader.process(request); + } filterChain.continueProcessing(request, channel); } diff --git a/shield/src/main/java/org/elasticsearch/shield/rest/action/RestShieldInfoAction.java b/shield/src/main/java/org/elasticsearch/shield/rest/action/RestShieldInfoAction.java index 86dd59d3137..46b33685ab1 100644 --- a/shield/src/main/java/org/elasticsearch/shield/rest/action/RestShieldInfoAction.java +++ b/shield/src/main/java/org/elasticsearch/shield/rest/action/RestShieldInfoAction.java @@ -12,11 +12,10 @@ import org.elasticsearch.common.inject.Inject; import org.elasticsearch.common.inject.internal.Nullable; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.xcontent.XContentBuilder; -import org.elasticsearch.license.plugin.core.LicenseState; import org.elasticsearch.rest.*; import org.elasticsearch.shield.ShieldBuild; import org.elasticsearch.shield.ShieldPlugin; -import org.elasticsearch.shield.license.LicenseService; +import org.elasticsearch.shield.license.ShieldLicenseState; import static org.elasticsearch.rest.RestRequest.Method.GET; import static org.elasticsearch.rest.RestRequest.Method.HEAD; @@ -24,14 +23,14 @@ import static org.elasticsearch.rest.RestRequest.Method.HEAD; public class RestShieldInfoAction extends BaseRestHandler { private final ClusterName clusterName; - private final LicenseService licenseService; + private final ShieldLicenseState shieldLicenseState; private final boolean shieldEnabled; @Inject - public RestShieldInfoAction(Settings settings, RestController controller, Client client, ClusterName clusterName, @Nullable LicenseService licenseService) { + public RestShieldInfoAction(Settings settings, RestController controller, Client client, ClusterName clusterName, @Nullable ShieldLicenseState licenseState) { super(settings, controller, client); this.clusterName = clusterName; - this.licenseService = licenseService; + this.shieldLicenseState = licenseState; this.shieldEnabled = ShieldPlugin.shieldEnabled(settings); controller.registerHandler(GET, "/_shield", this); controller.registerHandler(HEAD, "/_shield", this); @@ -72,7 +71,10 @@ public class RestShieldInfoAction extends BaseRestHandler { private Status resolveStatus() { if (shieldEnabled) { - if (licenseService.state() != LicenseState.DISABLED) { + assert shieldLicenseState != null; + // TODO this is error prone since the state could change between checks. We can also make this status better + // but we may remove this endpoint since it no longer serves much purpose + if (shieldLicenseState.securityEnabled() && shieldLicenseState.statsAndHealthEnabled()) { return Status.ENABLED; } return Status.UNLICENSED; diff --git a/shield/src/main/java/org/elasticsearch/shield/transport/ShieldServerTransportService.java b/shield/src/main/java/org/elasticsearch/shield/transport/ShieldServerTransportService.java index 5252e07a4cd..c694d6af651 100644 --- a/shield/src/main/java/org/elasticsearch/shield/transport/ShieldServerTransportService.java +++ b/shield/src/main/java/org/elasticsearch/shield/transport/ShieldServerTransportService.java @@ -12,6 +12,7 @@ import org.elasticsearch.shield.action.ShieldActionMapper; import org.elasticsearch.shield.authc.AuthenticationService; import org.elasticsearch.shield.authz.AuthorizationService; import org.elasticsearch.shield.authz.accesscontrol.RequestContext; +import org.elasticsearch.shield.license.ShieldLicenseState; import org.elasticsearch.shield.transport.netty.ShieldNettyTransport; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.*; @@ -35,6 +36,7 @@ public class ShieldServerTransportService extends TransportService { protected final AuthorizationService authzService; protected final ShieldActionMapper actionMapper; protected final ClientTransportFilter clientFilter; + protected final ShieldLicenseState licenseState; protected final Map profileFilters; @@ -43,12 +45,14 @@ public class ShieldServerTransportService extends TransportService { AuthenticationService authcService, AuthorizationService authzService, ShieldActionMapper actionMapper, - ClientTransportFilter clientTransportFilter) { + ClientTransportFilter clientTransportFilter, + ShieldLicenseState licenseState) { super(settings, transport, threadPool); this.authcService = authcService; this.authzService = authzService; this.actionMapper = actionMapper; this.clientFilter = clientTransportFilter; + this.licenseState = licenseState; this.profileFilters = initializeProfileFilters(); } @@ -64,13 +68,13 @@ public class ShieldServerTransportService extends TransportService { @Override public void registerRequestHandler(String action, Supplier requestFactory, String executor, TransportRequestHandler handler) { - TransportRequestHandler wrappedHandler = new ProfileSecuredRequestHandler<>(action, handler, profileFilters); + TransportRequestHandler wrappedHandler = new ProfileSecuredRequestHandler<>(action, handler, profileFilters, licenseState); super.registerRequestHandler(action, requestFactory, executor, wrappedHandler); } @Override public void registerRequestHandler(String action, Supplier request, String executor, boolean forceExecution, TransportRequestHandler handler) { - TransportRequestHandler wrappedHandler = new ProfileSecuredRequestHandler<>(action, handler, profileFilters); + TransportRequestHandler wrappedHandler = new ProfileSecuredRequestHandler<>(action, handler, profileFilters, licenseState); super.registerRequestHandler(action, request, executor, forceExecution, wrappedHandler); } @@ -104,7 +108,7 @@ public class ShieldServerTransportService extends TransportService { profileFilters.put(NettyTransport.DEFAULT_PROFILE, new ServerTransportFilter.NodeProfile(authcService, authzService, actionMapper, extractClientCert)); } - return profileFilters; + return Collections.unmodifiableMap(profileFilters); } ServerTransportFilter transportFilter(String profile) { @@ -116,30 +120,34 @@ public class ShieldServerTransportService extends TransportService { protected final String action; protected final TransportRequestHandler handler; private final Map profileFilters; + private final ShieldLicenseState licenseState; - public ProfileSecuredRequestHandler(String action, TransportRequestHandler handler, Map profileFilters) { + public ProfileSecuredRequestHandler(String action, TransportRequestHandler handler, Map profileFilters, ShieldLicenseState licenseState) { this.action = action; this.handler = handler; this.profileFilters = profileFilters; + this.licenseState = licenseState; } @Override @SuppressWarnings("unchecked") public void messageReceived(T request, TransportChannel channel) throws Exception { try { - String profile = channel.getProfileName(); - ServerTransportFilter filter = profileFilters.get(profile); + if (licenseState.securityEnabled()) { + String profile = channel.getProfileName(); + ServerTransportFilter filter = profileFilters.get(profile); - if (filter == null) { - if (TransportService.DIRECT_RESPONSE_PROFILE.equals(profile)) { - // apply the default filter to local requests. We never know what the request is or who sent it... - filter = profileFilters.get("default"); - } else { - throw new IllegalStateException("transport profile [" + profile + "] is not associated with a transport filter"); + if (filter == null) { + if (TransportService.DIRECT_RESPONSE_PROFILE.equals(profile)) { + // apply the default filter to local requests. We never know what the request is or who sent it... + filter = profileFilters.get("default"); + } else { + throw new IllegalStateException("transport profile [" + profile + "] is not associated with a transport filter"); + } } + assert filter != null; + filter.inbound(action, request, channel); } - assert filter != null; - filter.inbound(action, request, channel); RequestContext context = new RequestContext(request); RequestContext.setCurrent(context); handler.messageReceived(request, channel); diff --git a/shield/src/main/java/org/elasticsearch/shield/transport/filter/IPFilter.java b/shield/src/main/java/org/elasticsearch/shield/transport/filter/IPFilter.java index 9cbc69811cd..c565fa98612 100644 --- a/shield/src/main/java/org/elasticsearch/shield/transport/filter/IPFilter.java +++ b/shield/src/main/java/org/elasticsearch/shield/transport/filter/IPFilter.java @@ -20,6 +20,7 @@ import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.http.HttpServerTransport; import org.elasticsearch.node.settings.NodeSettingsService; import org.elasticsearch.shield.audit.AuditTrail; +import org.elasticsearch.shield.license.ShieldLicenseState; import org.elasticsearch.transport.Transport; import java.net.InetAddress; @@ -73,16 +74,19 @@ public class IPFilter extends AbstractLifecycleComponent { private NodeSettingsService nodeSettingsService; private final AuditTrail auditTrail; private final Transport transport; + private final ShieldLicenseState licenseState; private final boolean alwaysAllowBoundAddresses; private Map rules = Collections.EMPTY_MAP; private HttpServerTransport httpServerTransport = null; @Inject - public IPFilter(final Settings settings, AuditTrail auditTrail, NodeSettingsService nodeSettingsService, Transport transport) { + public IPFilter(final Settings settings, AuditTrail auditTrail, NodeSettingsService nodeSettingsService, + Transport transport, ShieldLicenseState licenseState) { super(settings); this.nodeSettingsService = nodeSettingsService; this.auditTrail = auditTrail; this.transport = transport; + this.licenseState = licenseState; this.alwaysAllowBoundAddresses = settings.getAsBoolean("shield.filter.always_allow_bound_address", true); } @@ -122,7 +126,12 @@ public class IPFilter extends AbstractLifecycleComponent { } public boolean accept(String profile, InetAddress peerAddress) { + if (licenseState.securityEnabled() == false) { + return true; + } + if (!rules.containsKey(profile)) { + // FIXME we need to audit here return true; } diff --git a/shield/src/test/java/org/elasticsearch/integration/LicensingTests.java b/shield/src/test/java/org/elasticsearch/integration/LicensingTests.java index 1be68833056..bd7e79dbb62 100644 --- a/shield/src/test/java/org/elasticsearch/integration/LicensingTests.java +++ b/shield/src/test/java/org/elasticsearch/integration/LicensingTests.java @@ -13,26 +13,31 @@ import org.elasticsearch.action.admin.cluster.stats.ClusterStatsResponse; import org.elasticsearch.action.admin.indices.stats.IndicesStatsResponse; import org.elasticsearch.action.index.IndexResponse; import org.elasticsearch.client.Client; +import org.elasticsearch.client.support.Headers; +import org.elasticsearch.client.transport.NoNodeAvailableException; +import org.elasticsearch.client.transport.TransportClient; 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.core.License; +import org.elasticsearch.license.core.License.OperationMode; import org.elasticsearch.license.plugin.core.LicenseState; import org.elasticsearch.license.plugin.core.Licensee; import org.elasticsearch.license.plugin.core.LicenseeRegistry; +import org.elasticsearch.node.Node; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.rest.RestStatus; -import org.elasticsearch.shield.license.LicenseService; +import org.elasticsearch.shield.ShieldPlugin; +import org.elasticsearch.shield.authc.support.UsernamePasswordToken; import org.elasticsearch.test.ShieldIntegTestCase; import org.elasticsearch.test.ShieldSettingsSource; +import org.elasticsearch.transport.Transport; +import org.junit.After; import org.junit.Test; -import java.util.ArrayList; -import java.util.Collection; -import java.util.Collections; -import java.util.List; +import java.util.*; import static org.elasticsearch.common.xcontent.XContentFactory.jsonBuilder; import static org.elasticsearch.test.hamcrest.ElasticsearchAssertions.assertNoFailures; @@ -95,6 +100,18 @@ public class LicensingTests extends ShieldIntegTestCase { return InternalLicensePlugin.NAME; } + @Override + public Settings nodeSettings(int nodeOrdinal) { + return Settings.builder().put(super.nodeSettings(nodeOrdinal)) + .put(Node.HTTP_ENABLED, true) + .build(); + } + + @After + public void resetLicensing() { + enableLicensing(); + } + @Test public void testEnableDisableBehaviour() throws Exception { IndexResponse indexResponse = index("test", "type", jsonBuilder() @@ -121,7 +138,7 @@ public class LicensingTests extends ShieldIntegTestCase { fail("expected an license expired exception when executing an index stats action"); } catch (ElasticsearchSecurityException ee) { // expected - assertThat(ee.getHeader("es.license.expired.feature"), hasItem(LicenseService.FEATURE_NAME)); + assertThat(ee.getHeader("es.license.expired.feature"), hasItem(ShieldPlugin.NAME)); assertThat(ee.status(), is(RestStatus.UNAUTHORIZED)); } @@ -130,7 +147,7 @@ public class LicensingTests extends ShieldIntegTestCase { fail("expected an license expired exception when executing cluster stats action"); } catch (ElasticsearchSecurityException ee) { // expected - assertThat(ee.getHeader("es.license.expired.feature"), hasItem(LicenseService.FEATURE_NAME)); + assertThat(ee.getHeader("es.license.expired.feature"), hasItem(ShieldPlugin.NAME)); assertThat(ee.status(), is(RestStatus.UNAUTHORIZED)); } @@ -139,7 +156,7 @@ public class LicensingTests extends ShieldIntegTestCase { fail("expected an license expired exception when executing cluster health action"); } catch (ElasticsearchSecurityException ee) { // expected - assertThat(ee.getHeader("es.license.expired.feature"), hasItem(LicenseService.FEATURE_NAME)); + assertThat(ee.getHeader("es.license.expired.feature"), hasItem(ShieldPlugin.NAME)); assertThat(ee.status(), is(RestStatus.UNAUTHORIZED)); } @@ -148,11 +165,11 @@ public class LicensingTests extends ShieldIntegTestCase { fail("expected an license expired exception when executing cluster health action"); } catch (ElasticsearchSecurityException ee) { // expected - assertThat(ee.getHeader("es.license.expired.feature"), hasItem(LicenseService.FEATURE_NAME)); + assertThat(ee.getHeader("es.license.expired.feature"), hasItem(ShieldPlugin.NAME)); assertThat(ee.status(), is(RestStatus.UNAUTHORIZED)); } - enableLicensing(); + enableLicensing(LicensingTests.generateLicense(randomFrom(OperationMode.values()))); IndicesStatsResponse indicesStatsResponse = client.admin().indices().prepareStats().get(); assertNoFailures(indicesStatsResponse); @@ -170,18 +187,77 @@ public class LicensingTests extends ShieldIntegTestCase { assertThat(nodeStats, notNullValue()); } + @Test + public void testRestAuthenticationByLicenseType() throws Exception { + // the default of the licensing tests is basic + assertThat(httpClient().path("/").execute().getStatusCode(), is(200)); + + // generate a new license with a mode that enables auth + OperationMode mode = randomFrom(OperationMode.GOLD, OperationMode.TRIAL, OperationMode.PLATINUM); + enableLicensing(generateLicense(mode)); + assertThat(httpClient().path("/").execute().getStatusCode(), is(401)); + } + + @Test + public void testTransportClientAuthenticationByLicenseType() throws Exception { + Settings.Builder builder = Settings.builder() + .put(internalCluster().transportClient().settings()); + // remove user info + builder.remove("shield.user"); + builder.remove(Headers.PREFIX + "." + UsernamePasswordToken.BASIC_AUTH_HEADER); + + // basic has no auth + try (TransportClient client = TransportClient.builder().settings(builder).addPlugin(ShieldPlugin.class).build()) { + client.addTransportAddress(internalCluster().getDataNodeInstance(Transport.class).boundAddress().publishAddress()); + assertGreenClusterState(client); + } + + // enable a license that enables security + OperationMode mode = randomFrom(OperationMode.GOLD, OperationMode.PLATINUM, OperationMode.TRIAL); + enableLicensing(generateLicense(mode)); + + try (TransportClient client = TransportClient.builder().settings(builder).addPlugin(ShieldPlugin.class).build()) { + client.addTransportAddress(internalCluster().getDataNodeInstance(Transport.class).boundAddress().publishAddress()); + client.admin().cluster().prepareHealth().get(); + fail("should not have been able to connect to a node!"); + } catch (NoNodeAvailableException e) { + // expected + } + } + public static void disableLicensing() { + disableLicensing(InternalLicenseeRegistry.DUMMY_LICENSE); + } + + public static void disableLicensing(License license) { for (InternalLicenseeRegistry service : internalCluster().getInstances(InternalLicenseeRegistry.class)) { - service.disable(); + service.disable(license); } } public static void enableLicensing() { + enableLicensing(InternalLicenseeRegistry.DUMMY_LICENSE); + } + + public static void enableLicensing(License license) { for (InternalLicenseeRegistry service : internalCluster().getInstances(InternalLicenseeRegistry.class)) { - service.enable(); + service.enable(license); } } + public static License generateLicense(OperationMode operationMode) { + return License.builder() + .expiryDate(System.currentTimeMillis()) + .issueDate(System.currentTimeMillis()) + .issuedTo("LicensingTests") + .issuer("test") + .maxNodes(Integer.MAX_VALUE) + .signature("_signature") + .type(operationMode.toString().toLowerCase(Locale.ROOT)) + .uid(String.valueOf(randomLong()) + System.identityHashCode(LicensingTests.class)) + .build(); + } + public static class InternalLicensePlugin extends Plugin { public static final String NAME = "internal-licensing"; @@ -228,24 +304,24 @@ public class LicensingTests extends ShieldIntegTestCase { @Inject public InternalLicenseeRegistry(Settings settings) { super(settings); - enable(); + enable(DUMMY_LICENSE); } @Override public void register(Licensee licensee) { licensees.add(licensee); - enable(); + enable(DUMMY_LICENSE); } - void enable() { + void enable(License license) { for (Licensee licensee : licensees) { - licensee.onChange(DUMMY_LICENSE, LicenseState.ENABLED); + licensee.onChange(license, LicenseState.ENABLED); } } - void disable() { + void disable(License license) { for (Licensee licensee : licensees) { - licensee.onChange(DUMMY_LICENSE, LicenseState.DISABLED); + licensee.onChange(license, LicenseState.DISABLED); } } } diff --git a/shield/src/test/java/org/elasticsearch/shield/ShieldPluginEnabledDisabledTests.java b/shield/src/test/java/org/elasticsearch/shield/ShieldPluginEnabledDisabledTests.java index 5063c81298a..dd24dca63f5 100644 --- a/shield/src/test/java/org/elasticsearch/shield/ShieldPluginEnabledDisabledTests.java +++ b/shield/src/test/java/org/elasticsearch/shield/ShieldPluginEnabledDisabledTests.java @@ -10,6 +10,7 @@ import org.apache.http.impl.client.HttpClients; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.http.HttpServerTransport; import org.elasticsearch.integration.LicensingTests; +import org.elasticsearch.license.core.License.OperationMode; import org.elasticsearch.node.Node; import org.elasticsearch.plugins.Plugin; import org.elasticsearch.shield.authc.support.SecuredString; @@ -104,11 +105,31 @@ public class ShieldPluginEnabledDisabledTests extends ShieldIntegTestCase { @Test public void testShieldInfoStatus() throws IOException { HttpServerTransport httpServerTransport = internalCluster().getDataNodeInstance(HttpServerTransport.class); + OperationMode mode; + if (enabled) { + mode = randomFrom(OperationMode.values()); + LicensingTests.enableLicensing(LicensingTests.generateLicense(mode)); + } else { + // this is the default right now + mode = OperationMode.BASIC; + } + try (CloseableHttpClient httpClient = HttpClients.createDefault()) { HttpResponse response = new HttpRequestBuilder(httpClient).httpTransport(httpServerTransport).method("GET").path("/_shield").addHeader(UsernamePasswordToken.BASIC_AUTH_HEADER, basicAuthHeaderValue(ShieldSettingsSource.DEFAULT_USER_NAME, new SecuredString(ShieldSettingsSource.DEFAULT_PASSWORD.toCharArray()))).execute(); assertThat(response.getStatusCode(), is(OK.getStatus())); - assertThat(new JsonPath(response.getBody()).evaluate("status").toString(), equalTo(enabled ? "enabled" : "disabled")); + + String expectedValue; + if (enabled) { + if (mode == OperationMode.BASIC) { + expectedValue = "unlicensed"; + } else { + expectedValue = "enabled"; + } + } else { + expectedValue = "disabled"; + } + assertThat(new JsonPath(response.getBody()).evaluate("status").toString(), equalTo(expectedValue)); if (enabled) { LicensingTests.disableLicensing(); diff --git a/shield/src/test/java/org/elasticsearch/shield/VersionCompatibilityTests.java b/shield/src/test/java/org/elasticsearch/shield/VersionCompatibilityTests.java index 5980ad6c75e..4d8a02b1779 100644 --- a/shield/src/test/java/org/elasticsearch/shield/VersionCompatibilityTests.java +++ b/shield/src/test/java/org/elasticsearch/shield/VersionCompatibilityTests.java @@ -6,6 +6,7 @@ package org.elasticsearch.shield; import org.elasticsearch.Version; +import org.elasticsearch.shield.license.ShieldLicensee; import org.elasticsearch.test.ESTestCase; import org.junit.Test; @@ -28,7 +29,7 @@ public class VersionCompatibilityTests extends ESTestCase { @Test public void testCompatibility() { /** - * see https://github.com/elasticsearch/elasticsearch/issues/9372 {@link org.elasticsearch.shield.license.LicenseService} + * see https://github.com/elasticsearch/elasticsearch/issues/9372 {@link ShieldLicensee} * Once es core supports merging cluster level custom metadata (licenses in our case), the tribe node will see some license coming from the tribe and everything will be ok. * */ diff --git a/shield/src/test/java/org/elasticsearch/shield/action/ShieldActionFilterTests.java b/shield/src/test/java/org/elasticsearch/shield/action/ShieldActionFilterTests.java index 99d0d9e7192..2c4a8fbd42c 100644 --- a/shield/src/test/java/org/elasticsearch/shield/action/ShieldActionFilterTests.java +++ b/shield/src/test/java/org/elasticsearch/shield/action/ShieldActionFilterTests.java @@ -11,14 +11,13 @@ import org.elasticsearch.action.ActionRequest; import org.elasticsearch.action.search.SearchScrollRequest; import org.elasticsearch.action.support.ActionFilterChain; import org.elasticsearch.common.settings.Settings; -import org.elasticsearch.license.plugin.core.LicenseState; import org.elasticsearch.shield.User; import org.elasticsearch.shield.action.interceptor.RequestInterceptor; import org.elasticsearch.shield.audit.AuditTrail; import org.elasticsearch.shield.authc.AuthenticationService; import org.elasticsearch.shield.authz.AuthorizationService; import org.elasticsearch.shield.crypto.CryptoService; -import org.elasticsearch.shield.license.LicenseEventsNotifier; +import org.elasticsearch.shield.license.ShieldLicenseState; import org.elasticsearch.test.ESTestCase; import org.junit.Before; import org.junit.Test; @@ -39,7 +38,7 @@ public class ShieldActionFilterTests extends ESTestCase { private AuthorizationService authzService; private CryptoService cryptoService; private AuditTrail auditTrail; - private LicenseEventsNotifier licenseEventsNotifier; + private ShieldLicenseState shieldLicenseState; private ShieldActionFilter filter; @Before @@ -48,8 +47,10 @@ public class ShieldActionFilterTests extends ESTestCase { authzService = mock(AuthorizationService.class); cryptoService = mock(CryptoService.class); auditTrail = mock(AuditTrail.class); - licenseEventsNotifier = new MockLicenseEventsNotifier(); - filter = new ShieldActionFilter(Settings.EMPTY, authcService, authzService, cryptoService, auditTrail, licenseEventsNotifier, new ShieldActionMapper(), new HashSet()); + shieldLicenseState = mock(ShieldLicenseState.class); + when(shieldLicenseState.securityEnabled()).thenReturn(true); + when(shieldLicenseState.statsAndHealthEnabled()).thenReturn(true); + filter = new ShieldActionFilter(Settings.EMPTY, authcService, authzService, cryptoService, auditTrail, shieldLicenseState, new ShieldActionMapper(), new HashSet()); } @Test @@ -110,10 +111,16 @@ public class ShieldActionFilterTests extends ESTestCase { verifyNoMoreInteractions(chain); } - private class MockLicenseEventsNotifier extends LicenseEventsNotifier { - @Override - public void register(MockLicenseEventsNotifier.Listener listener) { - listener.notify(LicenseState.ENABLED); - } + @Test + public void testApplyUnlicensed() throws Exception { + ActionRequest request = mock(ActionRequest.class); + ActionListener listener = mock(ActionListener.class); + ActionFilterChain chain = mock(ActionFilterChain.class); + when(shieldLicenseState.securityEnabled()).thenReturn(false); + filter.apply("_action", request, listener, chain); + verifyZeroInteractions(authcService); + verifyZeroInteractions(authzService); + verify(chain).proceed(eq("_action"), eq(request), eq(listener)); } + } diff --git a/shield/src/test/java/org/elasticsearch/shield/authz/accesscontrol/ShieldIndexSearcherWrapperIntegrationTests.java b/shield/src/test/java/org/elasticsearch/shield/authz/accesscontrol/ShieldIndexSearcherWrapperIntegrationTests.java index dfe6818be90..4a7c365316c 100644 --- a/shield/src/test/java/org/elasticsearch/shield/authz/accesscontrol/ShieldIndexSearcherWrapperIntegrationTests.java +++ b/shield/src/test/java/org/elasticsearch/shield/authz/accesscontrol/ShieldIndexSearcherWrapperIntegrationTests.java @@ -37,8 +37,8 @@ import org.elasticsearch.index.mapper.MapperService; import org.elasticsearch.index.query.IndexQueryParserService; import org.elasticsearch.index.query.ParsedQuery; import org.elasticsearch.index.shard.ShardId; -import org.elasticsearch.indices.IndicesLifecycle; import org.elasticsearch.shield.authz.InternalAuthorizationService; +import org.elasticsearch.shield.license.ShieldLicenseState; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.transport.TransportRequest; import org.mockito.Matchers; @@ -78,7 +78,6 @@ public class ShieldIndexSearcherWrapperIntegrationTests extends ESTestCase { request.putInContext(InternalAuthorizationService.INDICES_PERMISSIONS_KEY, new IndicesAccessControl(true, singletonMap("_index", indexAccessControl))); IndexQueryParserService parserService = mock(IndexQueryParserService.class); - IndicesLifecycle indicesLifecycle = mock(IndicesLifecycle.class); BitsetFilterCache bitsetFilterCache = mock(BitsetFilterCache.class); when(bitsetFilterCache.getBitSetProducer(Matchers.any(Query.class))).then(new Answer() { @Override @@ -100,8 +99,10 @@ public class ShieldIndexSearcherWrapperIntegrationTests extends ESTestCase { }; } }); + ShieldLicenseState licenseState = mock(ShieldLicenseState.class); + when(licenseState.documentAndFieldLevelSecurityEnabled()).thenReturn(true); ShieldIndexSearcherWrapper wrapper = new ShieldIndexSearcherWrapper( - Settings.EMPTY, parserService, mapperService, bitsetFilterCache + Settings.EMPTY, parserService, mapperService, bitsetFilterCache, licenseState ); Directory directory = newDirectory(); diff --git a/shield/src/test/java/org/elasticsearch/shield/authz/accesscontrol/ShieldIndexSearcherWrapperUnitTests.java b/shield/src/test/java/org/elasticsearch/shield/authz/accesscontrol/ShieldIndexSearcherWrapperUnitTests.java index b34c5ae529f..304e87242d2 100644 --- a/shield/src/test/java/org/elasticsearch/shield/authz/accesscontrol/ShieldIndexSearcherWrapperUnitTests.java +++ b/shield/src/test/java/org/elasticsearch/shield/authz/accesscontrol/ShieldIndexSearcherWrapperUnitTests.java @@ -47,6 +47,7 @@ import org.elasticsearch.index.similarity.SimilarityService; import org.elasticsearch.script.ScriptService; import org.elasticsearch.search.aggregations.LeafBucketCollector; import org.elasticsearch.shield.authz.InternalAuthorizationService; +import org.elasticsearch.shield.license.ShieldLicenseState; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.transport.TransportRequest; import org.junit.After; @@ -73,6 +74,7 @@ public class ShieldIndexSearcherWrapperUnitTests extends ESTestCase { private MapperService mapperService; private ShieldIndexSearcherWrapper shieldIndexSearcherWrapper; private ElasticsearchDirectoryReader esIn; + private ShieldLicenseState licenseState; @Before public void before() throws Exception { @@ -84,7 +86,9 @@ public class ShieldIndexSearcherWrapperUnitTests extends ESTestCase { mapperService = new MapperService(index, settings, analysisService, similarityService, scriptService); shardId = new ShardId(index, 0); - shieldIndexSearcherWrapper = new ShieldIndexSearcherWrapper(settings, null, mapperService, null); + licenseState = mock(ShieldLicenseState.class); + when(licenseState.documentAndFieldLevelSecurityEnabled()).thenReturn(true); + shieldIndexSearcherWrapper = new ShieldIndexSearcherWrapper(settings, null, mapperService, null, licenseState); IndexShard indexShard = mock(IndexShard.class); when(indexShard.shardId()).thenReturn(shardId); @@ -130,6 +134,21 @@ public class ShieldIndexSearcherWrapperUnitTests extends ESTestCase { assertThat(result.getFieldNames().contains("_all"), is(false)); // _all contains actual user data and therefor can't be included by default } + public void testWrapReaderWhenFeatureDisabled() throws Exception { + when(licenseState.documentAndFieldLevelSecurityEnabled()).thenReturn(false); + DirectoryReader reader = shieldIndexSearcherWrapper.wrap(esIn); + assertThat(reader, sameInstance(esIn)); + } + + public void testWrapSearcherWhenFeatureDisabled() throws Exception { + ShardId shardId = new ShardId("_index", 0); + EngineConfig engineConfig = new EngineConfig(shardId, null, null, Settings.EMPTY, null, null, null, null, null, null, new BM25Similarity(), null, null, null, new NoneQueryCache(shardId.index(), Settings.EMPTY), QueryCachingPolicy.ALWAYS_CACHE, null); // can't mock... + + IndexSearcher indexSearcher = new IndexSearcher(esIn); + IndexSearcher result = shieldIndexSearcherWrapper.wrap(engineConfig, indexSearcher); + assertThat(result, sameInstance(indexSearcher)); + } + public void testWildcards() throws Exception { XContentBuilder mappingSource = jsonBuilder().startObject().startObject("type").startObject("properties") .startObject("field1_a").field("type", "string").endObject() diff --git a/shield/src/test/java/org/elasticsearch/shield/license/ShieldLicenseStateTests.java b/shield/src/test/java/org/elasticsearch/shield/license/ShieldLicenseStateTests.java new file mode 100644 index 00000000000..4d24e0aed82 --- /dev/null +++ b/shield/src/test/java/org/elasticsearch/shield/license/ShieldLicenseStateTests.java @@ -0,0 +1,80 @@ +/* + * 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.core.License; +import org.elasticsearch.license.plugin.core.LicenseState; +import org.elasticsearch.license.plugin.core.Licensee; +import org.elasticsearch.test.ESTestCase; + +import static org.hamcrest.Matchers.*; + +/** + * Unit tests for the {@link ShieldLicenseState} + */ +public class ShieldLicenseStateTests extends ESTestCase { + + public void testDefaults() { + ShieldLicenseState licenseState = new ShieldLicenseState(); + assertThat(licenseState.securityEnabled(), is(true)); + assertThat(licenseState.statsAndHealthEnabled(), is(true)); + assertThat(licenseState.documentAndFieldLevelSecurityEnabled(), is(true)); + } + + public void testBasic() { + ShieldLicenseState licenseState = new ShieldLicenseState(); + licenseState.updateStatus(new Licensee.Status(License.OperationMode.BASIC, randomBoolean() ? LicenseState.ENABLED : LicenseState.GRACE_PERIOD)); + + assertThat(licenseState.securityEnabled(), is(false)); + assertThat(licenseState.statsAndHealthEnabled(), is(true)); + assertThat(licenseState.documentAndFieldLevelSecurityEnabled(), is(false)); + } + + public void testBasicExpired() { + ShieldLicenseState licenseState = new ShieldLicenseState(); + licenseState.updateStatus(new Licensee.Status(License.OperationMode.BASIC, LicenseState.DISABLED)); + + assertThat(licenseState.securityEnabled(), is(false)); + assertThat(licenseState.statsAndHealthEnabled(), is(false)); + assertThat(licenseState.documentAndFieldLevelSecurityEnabled(), is(false)); + } + + public void testGold() { + ShieldLicenseState licenseState = new ShieldLicenseState(); + licenseState.updateStatus(new Licensee.Status(License.OperationMode.GOLD, randomBoolean() ? LicenseState.ENABLED : LicenseState.GRACE_PERIOD)); + + assertThat(licenseState.securityEnabled(), is(true)); + assertThat(licenseState.statsAndHealthEnabled(), is(true)); + assertThat(licenseState.documentAndFieldLevelSecurityEnabled(), is(false)); + } + + public void testGoldExpired() { + ShieldLicenseState licenseState = new ShieldLicenseState(); + licenseState.updateStatus(new Licensee.Status(License.OperationMode.GOLD, LicenseState.DISABLED)); + + assertThat(licenseState.securityEnabled(), is(true)); + assertThat(licenseState.statsAndHealthEnabled(), is(false)); + assertThat(licenseState.documentAndFieldLevelSecurityEnabled(), is(false)); + } + + public void testPlatinum() { + ShieldLicenseState licenseState = new ShieldLicenseState(); + licenseState.updateStatus(new Licensee.Status(License.OperationMode.PLATINUM, randomBoolean() ? LicenseState.ENABLED : LicenseState.GRACE_PERIOD)); + + assertThat(licenseState.securityEnabled(), is(true)); + assertThat(licenseState.statsAndHealthEnabled(), is(true)); + assertThat(licenseState.documentAndFieldLevelSecurityEnabled(), is(true)); + } + + public void testPlatinumExpired() { + ShieldLicenseState licenseState = new ShieldLicenseState(); + licenseState.updateStatus(new Licensee.Status(License.OperationMode.PLATINUM, LicenseState.DISABLED)); + + assertThat(licenseState.securityEnabled(), is(true)); + assertThat(licenseState.statsAndHealthEnabled(), is(false)); + assertThat(licenseState.documentAndFieldLevelSecurityEnabled(), is(true)); + } +} diff --git a/shield/src/test/java/org/elasticsearch/shield/rest/ShieldRestFilterTests.java b/shield/src/test/java/org/elasticsearch/shield/rest/ShieldRestFilterTests.java index 73ca0366161..3a8272c0cb4 100644 --- a/shield/src/test/java/org/elasticsearch/shield/rest/ShieldRestFilterTests.java +++ b/shield/src/test/java/org/elasticsearch/shield/rest/ShieldRestFilterTests.java @@ -13,6 +13,7 @@ import org.elasticsearch.rest.RestFilterChain; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.shield.User; import org.elasticsearch.shield.authc.AuthenticationService; +import org.elasticsearch.shield.license.ShieldLicenseState; import org.elasticsearch.test.ESTestCase; import org.junit.Before; import org.junit.Test; @@ -30,6 +31,7 @@ public class ShieldRestFilterTests extends ESTestCase { private RestChannel channel; private RestFilterChain chain; private ShieldRestFilter filter; + private ShieldLicenseState licenseState; @Before public void init() throws Exception { @@ -37,7 +39,9 @@ public class ShieldRestFilterTests extends ESTestCase { RestController restController = mock(RestController.class); channel = mock(RestChannel.class); chain = mock(RestFilterChain.class); - filter = new ShieldRestFilter(authcService, restController, Settings.EMPTY); + licenseState = mock(ShieldLicenseState.class); + when(licenseState.securityEnabled()).thenReturn(true); + filter = new ShieldRestFilter(authcService, restController, Settings.EMPTY, licenseState); verify(restController).registerFilter(filter); } @@ -51,6 +55,15 @@ public class ShieldRestFilterTests extends ESTestCase { verifyZeroInteractions(channel); } + @Test + public void testProcessBasicLicense() throws Exception { + RestRequest request = mock(RestRequest.class); + when(licenseState.securityEnabled()).thenReturn(false); + filter.process(request, channel, chain); + verify(chain).continueProcessing(request, channel); + verifyZeroInteractions(channel, authcService); + } + @Test public void testProcess_AuthenticationError() throws Exception { RestRequest request = mock(RestRequest.class); diff --git a/shield/src/test/java/org/elasticsearch/shield/transport/TransportFilterTests.java b/shield/src/test/java/org/elasticsearch/shield/transport/TransportFilterTests.java index 74d1e1752a7..c2d2cbce353 100644 --- a/shield/src/test/java/org/elasticsearch/shield/transport/TransportFilterTests.java +++ b/shield/src/test/java/org/elasticsearch/shield/transport/TransportFilterTests.java @@ -18,6 +18,7 @@ import org.elasticsearch.plugins.Plugin; import org.elasticsearch.shield.action.ShieldActionMapper; import org.elasticsearch.shield.authc.AuthenticationService; import org.elasticsearch.shield.authz.AuthorizationService; +import org.elasticsearch.shield.license.ShieldLicenseState; import org.elasticsearch.test.ESIntegTestCase; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.transport.*; @@ -298,7 +299,8 @@ public class TransportFilterTests extends ESIntegTestCase { @Inject public InternalPluginServerTransportService(Settings settings, Transport transport, ThreadPool threadPool, AuthenticationService authcService, AuthorizationService authzService, ShieldActionMapper actionMapper, ClientTransportFilter clientTransportFilter) { - super(settings, transport, threadPool, authcService, authzService, actionMapper, clientTransportFilter); + super(settings, transport, threadPool, authcService, authzService, actionMapper, clientTransportFilter, mock(ShieldLicenseState.class)); + when(licenseState.securityEnabled()).thenReturn(true); } protected Map initializeProfileFilters() { diff --git a/shield/src/test/java/org/elasticsearch/shield/transport/filter/IPFilterTests.java b/shield/src/test/java/org/elasticsearch/shield/transport/filter/IPFilterTests.java index dfed968f21f..8c92e710319 100644 --- a/shield/src/test/java/org/elasticsearch/shield/transport/filter/IPFilterTests.java +++ b/shield/src/test/java/org/elasticsearch/shield/transport/filter/IPFilterTests.java @@ -15,6 +15,7 @@ import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.http.HttpServerTransport; import org.elasticsearch.node.settings.NodeSettingsService; import org.elasticsearch.shield.audit.AuditTrail; +import org.elasticsearch.shield.license.ShieldLicenseState; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.test.junit.annotations.Network; import org.elasticsearch.transport.Transport; @@ -35,6 +36,7 @@ import static org.mockito.Mockito.*; public class IPFilterTests extends ESTestCase { private IPFilter ipFilter; + private ShieldLicenseState licenseState; private AuditTrail auditTrail; private Transport transport; private HttpServerTransport httpTransport; @@ -42,6 +44,8 @@ public class IPFilterTests extends ESTestCase { @Before public void init() { + licenseState = mock(ShieldLicenseState.class); + when(licenseState.securityEnabled()).thenReturn(true); auditTrail = mock(AuditTrail.class); nodeSettingsService = mock(NodeSettingsService.class); @@ -66,7 +70,7 @@ public class IPFilterTests extends ESTestCase { .put("shield.transport.filter.allow", "127.0.0.1") .put("shield.transport.filter.deny", "10.0.0.0/8") .build(); - ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport, licenseState).start(); assertAddressIsAllowed("127.0.0.1"); assertAddressIsDenied("10.2.3.4"); @@ -80,7 +84,7 @@ public class IPFilterTests extends ESTestCase { .put("shield.transport.filter.allow", "2001:0db8:1234::/48") .putArray("shield.transport.filter.deny", "1234:db8:85a3:0:0:8a2e:370:7334", "4321:db8:1234::/48") .build(); - ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport, licenseState).start(); assertAddressIsAllowed("2001:0db8:1234:0000:0000:8a2e:0370:7334"); assertAddressIsDenied("1234:0db8:85a3:0000:0000:8a2e:0370:7334"); @@ -94,7 +98,7 @@ public class IPFilterTests extends ESTestCase { .put("shield.transport.filter.allow", "127.0.0.1") .put("shield.transport.filter.deny", "*.google.com") .build(); - ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport, licenseState).start(); assertAddressIsAllowed("127.0.0.1"); assertAddressIsDenied("8.8.8.8"); @@ -105,7 +109,7 @@ public class IPFilterTests extends ESTestCase { Settings settings = settingsBuilder() .put("shield.transport.filter.allow", "_all") .build(); - ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport, licenseState).start(); assertAddressIsAllowed("127.0.0.1"); assertAddressIsAllowed("173.194.70.100"); @@ -119,7 +123,7 @@ public class IPFilterTests extends ESTestCase { .put("transport.profiles.client.shield.filter.allow", "192.168.0.1") .put("transport.profiles.client.shield.filter.deny", "_all") .build(); - ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport, licenseState).start(); assertAddressIsAllowed("127.0.0.1"); assertAddressIsDenied("192.168.0.1"); @@ -133,7 +137,7 @@ public class IPFilterTests extends ESTestCase { .put("shield.transport.filter.allow", "10.0.0.1") .put("shield.transport.filter.deny", "10.0.0.0/8") .build(); - ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport, licenseState).start(); assertAddressIsAllowed("10.0.0.1"); assertAddressIsDenied("10.0.0.2"); @@ -142,7 +146,7 @@ public class IPFilterTests extends ESTestCase { @Test public void testDefaultAllow() throws Exception { Settings settings = settingsBuilder().build(); - ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport, licenseState).start(); assertAddressIsAllowed("10.0.0.1"); assertAddressIsAllowed("10.0.0.2"); @@ -156,7 +160,7 @@ public class IPFilterTests extends ESTestCase { .put("shield.http.filter.allow", "10.0.0.0/8") .put("shield.http.filter.deny", "192.168.0.1") .build(); - ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport, licenseState).start(); ipFilter.setHttpServerTransport(httpTransport); assertAddressIsAllowedForProfile(IPFilter.HTTP_PROFILE_NAME, "10.2.3.4"); @@ -169,7 +173,7 @@ public class IPFilterTests extends ESTestCase { .put("shield.transport.filter.allow", "127.0.0.1") .put("shield.transport.filter.deny", "10.0.0.0/8") .build(); - ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport, licenseState).start(); ipFilter.setHttpServerTransport(httpTransport); assertAddressIsAllowedForProfile(IPFilter.HTTP_PROFILE_NAME, "127.0.0.1"); @@ -189,7 +193,7 @@ public class IPFilterTests extends ESTestCase { } else { settings = settingsBuilder().put("shield.transport.filter.deny", "_all").build(); } - ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport).start(); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport, licenseState).start(); ipFilter.setHttpServerTransport(httpTransport); for (String addressString : addressStrings) { @@ -198,6 +202,26 @@ public class IPFilterTests extends ESTestCase { } } + @Test + public void testThatAllAddressesAreAllowedWhenLicenseDisablesSecurity() { + Settings settings = settingsBuilder() + .put("shield.transport.filter.deny", "_all") + .build(); + when(licenseState.securityEnabled()).thenReturn(false); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport, licenseState).start(); + + // don't use the assert helper because we don't want the audit trail to be invoked here + String message = String.format(Locale.ROOT, "Expected address %s to be allowed", "8.8.8.8"); + InetAddress address = InetAddresses.forString("8.8.8.8"); + assertThat(message, ipFilter.accept("default", address), is(true)); + verifyZeroInteractions(auditTrail); + + // for sanity enable license and check that it is denied + when(licenseState.securityEnabled()).thenReturn(true); + ipFilter = new IPFilter(settings, auditTrail, nodeSettingsService, transport, licenseState).start(); + assertAddressIsDeniedForProfile("default", "8.8.8.8"); + } + private void assertAddressIsAllowedForProfile(String profile, String ... inetAddresses) { for (String inetAddress : inetAddresses) { String message = String.format(Locale.ROOT, "Expected address %s to be allowed", inetAddress); diff --git a/shield/src/test/java/org/elasticsearch/shield/transport/netty/IPFilterNettyUpstreamHandlerTests.java b/shield/src/test/java/org/elasticsearch/shield/transport/netty/IPFilterNettyUpstreamHandlerTests.java index f0f35554ab6..478bda5fa54 100644 --- a/shield/src/test/java/org/elasticsearch/shield/transport/netty/IPFilterNettyUpstreamHandlerTests.java +++ b/shield/src/test/java/org/elasticsearch/shield/transport/netty/IPFilterNettyUpstreamHandlerTests.java @@ -14,6 +14,7 @@ import org.elasticsearch.common.transport.TransportAddress; import org.elasticsearch.http.HttpServerTransport; import org.elasticsearch.node.settings.NodeSettingsService; import org.elasticsearch.shield.audit.AuditTrail; +import org.elasticsearch.shield.license.ShieldLicenseState; import org.elasticsearch.shield.transport.filter.IPFilter; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.transport.Transport; @@ -52,7 +53,9 @@ public class IPFilterNettyUpstreamHandlerTests extends ESTestCase { when(transport.lifecycleState()).thenReturn(Lifecycle.State.STARTED); NodeSettingsService nodeSettingsService = mock(NodeSettingsService.class); - IPFilter ipFilter = new IPFilter(settings, AuditTrail.NOOP, nodeSettingsService, transport).start(); + ShieldLicenseState licenseState = mock(ShieldLicenseState.class); + when(licenseState.securityEnabled()).thenReturn(true); + IPFilter ipFilter = new IPFilter(settings, AuditTrail.NOOP, nodeSettingsService, transport, licenseState).start(); if (isHttpEnabled) { HttpServerTransport httpTransport = mock(HttpServerTransport.class); diff --git a/shield/src/test/java/org/elasticsearch/test/ShieldIntegTestCase.java b/shield/src/test/java/org/elasticsearch/test/ShieldIntegTestCase.java index 1df9c56ea2a..18c5b27f0b7 100644 --- a/shield/src/test/java/org/elasticsearch/test/ShieldIntegTestCase.java +++ b/shield/src/test/java/org/elasticsearch/test/ShieldIntegTestCase.java @@ -12,7 +12,6 @@ import org.elasticsearch.action.admin.cluster.node.info.NodesInfoResponse; import org.elasticsearch.client.Client; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.plugins.Plugin; -import org.elasticsearch.plugins.PluginInfo; import org.elasticsearch.shield.ShieldPlugin; import org.elasticsearch.shield.authc.support.SecuredString; import org.elasticsearch.test.ESIntegTestCase.SuppressLocalMode; @@ -22,8 +21,6 @@ import org.junit.BeforeClass; import org.junit.Rule; import org.junit.rules.ExternalResource; -import java.io.IOException; -import java.net.InetSocketAddress; import java.nio.file.Path; import java.util.*; import java.util.stream.Collectors;