From 0ee16d0115703860e3ed24ff47c5b07ad1206587 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Thu, 11 Apr 2019 10:30:56 +1000 Subject: [PATCH] Security on Basic License This adds support for using security on a basic license. It includes: - AllowedRealmType.NATIVE realms (reserved, native, file) - Roles / RBAC - TLS (already supported) It does not support: - Audit - IP filters - Token Service & API Keys - Advanced realms (AD, LDAP, SAML, etc) - Advanced roles (DLS, FLS) - Pluggable security As with trial licences, security is disabled by default. This commit does not include any new automated tests, but existing tests have been updated. --- .../org/elasticsearch/license/License.java | 4 + .../license/XPackLicenseState.java | 57 ++++++-- .../core/action/TransportXPackInfoAction.java | 3 +- .../license/XPackLicenseStateTests.java | 54 +++++-- .../xpack/security/Security.java | 18 +-- .../xpack/security/SecurityFeatureSet.java | 2 +- .../action/filter/SecurityActionFilter.java | 5 +- .../xpack/security/authc/ApiKeyService.java | 132 ++++++++++-------- .../xpack/security/authc/TokenService.java | 20 ++- .../rest/action/SecurityBaseRestHandler.java | 5 +- .../action/apikey/ApiKeyBaseRestHandler.java | 35 +++++ .../{ => apikey}/RestCreateApiKeyAction.java | 4 +- .../{ => apikey}/RestGetApiKeyAction.java | 4 +- .../RestInvalidateApiKeyAction.java | 4 +- .../action/oauth2/RestGetTokenAction.java | 3 +- .../oauth2/RestInvalidateTokenAction.java | 3 +- .../action/oauth2/TokenBaseRestHandler.java | 40 ++++++ .../elasticsearch/license/LicensingTests.java | 4 +- .../security/SecurityFeatureSetTests.java | 4 +- ...ansportOpenIdConnectLogoutActionTests.java | 8 +- ...sportSamlInvalidateSessionActionTests.java | 6 +- .../saml/TransportSamlLogoutActionTests.java | 5 +- .../TransportCreateTokenActionTests.java | 13 +- .../security/authc/ApiKeyServiceTests.java | 129 +++++++++++++---- .../authc/AuthenticationServiceTests.java | 7 + .../security/authc/TokenServiceTests.java | 128 ++++++++--------- .../action/SecurityBaseRestHandlerTests.java | 11 +- .../RestCreateApiKeyActionTests.java | 4 +- .../RestGetApiKeyActionTests.java | 4 +- .../RestInvalidateApiKeyActionTests.java | 4 +- .../action/saml/SamlBaseRestHandlerTests.java | 13 +- .../xpack/security/test/SecurityMocks.java | 94 +++++++++++++ 32 files changed, 607 insertions(+), 220 deletions(-) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/ApiKeyBaseRestHandler.java rename x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/{ => apikey}/RestCreateApiKeyAction.java (94%) rename x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/{ => apikey}/RestGetApiKeyAction.java (95%) rename x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/{ => apikey}/RestInvalidateApiKeyAction.java (95%) create mode 100644 x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oauth2/TokenBaseRestHandler.java rename x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/{ => apikey}/RestCreateApiKeyActionTests.java (95%) rename x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/{ => apikey}/RestGetApiKeyActionTests.java (97%) rename x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/{ => apikey}/RestInvalidateApiKeyActionTests.java (96%) create mode 100644 x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/test/SecurityMocks.java diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java index 158c0eb7b2e..62ffd76e8ea 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/License.java @@ -126,6 +126,10 @@ public class License implements ToXContentObject { throw new IllegalArgumentException("unknown type [" + type + "]"); } } + + public String description() { + return name().toLowerCase(Locale.ROOT); + } } private License(int version, String uid, String issuer, String issuedTo, long issueDate, String type, diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java index a5da656b886..51b8894a54e 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/license/XPackLicenseState.java @@ -331,8 +331,17 @@ public class XPackLicenseState { OperationMode mode = status.mode; final boolean isSecurityCurrentlyEnabled = isSecurityEnabled(mode, isSecurityExplicitlyEnabled, isSecurityEnabled); - return isSecurityCurrentlyEnabled && (mode == OperationMode.STANDARD || mode == OperationMode.GOLD - || mode == OperationMode.PLATINUM || mode == OperationMode.TRIAL); + if (isSecurityCurrentlyEnabled) { + switch (mode) { + case BASIC: + case STANDARD: + case GOLD: + case PLATINUM: + case TRIAL: + return true; + } + } + return false; } /** @@ -405,6 +414,7 @@ public class XPackLicenseState { return AllowedRealmType.ALL; case GOLD: return AllowedRealmType.DEFAULT; + case BASIC: case STANDARD: return AllowedRealmType.NATIVE; default: @@ -425,6 +435,24 @@ public class XPackLicenseState { && status.active; } + /** + * @return whether the Elasticsearch {@code TokenService} is allowed based on the license {@link OperationMode} + */ + public synchronized boolean isTokenServiceAllowed() { + final OperationMode mode = status.mode; + final boolean isSecurityCurrentlyEnabled = isSecurityEnabled(mode, isSecurityExplicitlyEnabled, isSecurityEnabled); + return isSecurityCurrentlyEnabled && (mode == OperationMode.GOLD || mode == OperationMode.PLATINUM || mode == OperationMode.TRIAL); + } + + /** + * @return whether the Elasticsearch {@code ApiKeyService} is allowed based on the license {@link OperationMode} + */ + public synchronized boolean isApiKeyServiceAllowed() { + final OperationMode mode = status.mode; + final boolean isSecurityCurrentlyEnabled = isSecurityEnabled(mode, isSecurityExplicitlyEnabled, isSecurityEnabled); + return isSecurityCurrentlyEnabled && (mode == OperationMode.GOLD || mode == OperationMode.PLATINUM || mode == OperationMode.TRIAL); + } + /** * @return whether "authorization_realms" are allowed based on the license {@link OperationMode} * @see org.elasticsearch.xpack.core.security.authc.support.DelegatedAuthorizationSettings @@ -674,24 +702,35 @@ public class XPackLicenseState { public synchronized boolean isSecurityAvailable() { OperationMode mode = status.mode; return mode == OperationMode.GOLD || mode == OperationMode.PLATINUM || mode == OperationMode.STANDARD || - mode == OperationMode.TRIAL; + mode == OperationMode.TRIAL || mode == OperationMode.BASIC; } /** - * @return true if security has been disabled by a trial license which is the case of the - * default distribution post 6.3.0. The conditions necessary for this are: + * @return true if security has been disabled due it being the default setting for this license type. + * The conditions necessary for this are: * */ - public synchronized boolean isSecurityDisabledByTrialLicense() { - return status.mode == OperationMode.TRIAL && isSecurityEnabled && isSecurityExplicitlyEnabled == false; + public synchronized boolean isSecurityDisabledByLicenseDefaults() { + switch (status.mode) { + case TRIAL: + case BASIC: + return isSecurityEnabled && isSecurityExplicitlyEnabled == false; + } + return false; } private static boolean isSecurityEnabled(final OperationMode mode, final boolean isSecurityExplicitlyEnabled, final boolean isSecurityEnabled) { - return mode == OperationMode.TRIAL ? isSecurityExplicitlyEnabled : isSecurityEnabled; + switch (mode) { + case TRIAL: + case BASIC: + return isSecurityExplicitlyEnabled; + default: + return isSecurityEnabled; + } } /** diff --git a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/TransportXPackInfoAction.java b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/TransportXPackInfoAction.java index 541bd4e2fce..16f4541e0a7 100644 --- a/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/TransportXPackInfoAction.java +++ b/x-pack/plugin/core/src/main/java/org/elasticsearch/xpack/core/action/TransportXPackInfoAction.java @@ -20,7 +20,6 @@ import org.elasticsearch.transport.TransportService; import org.elasticsearch.xpack.core.XPackBuild; import org.elasticsearch.xpack.core.XPackFeatureSet; -import java.util.Locale; import java.util.Set; import java.util.stream.Collectors; @@ -51,7 +50,7 @@ public class TransportXPackInfoAction extends HandledTransportAction roleDescriptorSet, + public void createApiKey(Authentication authentication, CreateApiKeyRequest request, Set userRoles, ActionListener listener) { ensureEnabled(); if (authentication == null) { @@ -191,12 +194,12 @@ public class ApiKeyService { * this check is best effort as there could be two nodes executing search and * then index concurrently allowing a duplicate name. */ - checkDuplicateApiKeyNameAndCreateApiKey(authentication, request, roleDescriptorSet, listener); + checkDuplicateApiKeyNameAndCreateApiKey(authentication, request, userRoles, listener); } } private void checkDuplicateApiKeyNameAndCreateApiKey(Authentication authentication, CreateApiKeyRequest request, - Set roleDescriptorSet, + Set userRoles, ActionListener listener) { final BoolQueryBuilder boolQuery = QueryBuilders.boolQuery() .filter(QueryBuilders.termQuery("doc_type", "api_key")) @@ -221,7 +224,7 @@ public class ApiKeyService { listener.onFailure(traceLog("create api key", new ElasticsearchSecurityException( "Error creating api key as api key with name [{}] already exists", request.getName()))); } else { - createApiKeyAndIndexIt(authentication, request, roleDescriptorSet, listener); + createApiKeyAndIndexIt(authentication, request, userRoles, listener); } }, listener::onFailure))); @@ -240,51 +243,8 @@ public class ApiKeyService { Version.V_6_7_0); } - final char[] keyHash = hasher.hash(apiKey); - try (XContentBuilder builder = XContentFactory.jsonBuilder()) { - builder.startObject() - .field("doc_type", "api_key") - .field("creation_time", created.toEpochMilli()) - .field("expiration_time", expiration == null ? null : expiration.toEpochMilli()) - .field("api_key_invalidated", false); - - byte[] utf8Bytes = null; - try { - utf8Bytes = CharArrays.toUtf8Bytes(keyHash); - builder.field("api_key_hash").utf8Value(utf8Bytes, 0, utf8Bytes.length); - } finally { - if (utf8Bytes != null) { - Arrays.fill(utf8Bytes, (byte) 0); - } - } - - // Save role_descriptors - builder.startObject("role_descriptors"); - if (request.getRoleDescriptors() != null && request.getRoleDescriptors().isEmpty() == false) { - for (RoleDescriptor descriptor : request.getRoleDescriptors()) { - builder.field(descriptor.getName(), - (contentBuilder, params) -> descriptor.toXContent(contentBuilder, params, true)); - } - } - builder.endObject(); - - // Save limited_by_role_descriptors - builder.startObject("limited_by_role_descriptors"); - for (RoleDescriptor descriptor : roleDescriptorSet) { - builder.field(descriptor.getName(), - (contentBuilder, params) -> descriptor.toXContent(contentBuilder, params, true)); - } - builder.endObject(); - - builder.field("name", request.getName()) - .field("version", version.id) - .startObject("creator") - .field("principal", authentication.getUser().principal()) - .field("metadata", authentication.getUser().metadata()) - .field("realm", authentication.getLookedUpBy() == null ? - authentication.getAuthenticatedBy().getName() : authentication.getLookedUpBy().getName()) - .endObject() - .endObject(); + try (XContentBuilder builder = newDocument(apiKey, request.getName(), authentication, roleDescriptorSet, created, expiration, + request.getRoleDescriptors(), version)) { final IndexRequest indexRequest = client.prepareIndex(SECURITY_MAIN_ALIAS, SINGLE_MAPPING_NAME) .setSource(builder) @@ -298,9 +258,64 @@ public class ApiKeyService { listener::onFailure))); } catch (IOException e) { listener.onFailure(e); + } + } + + /** + * package protected for testing + */ + XContentBuilder newDocument(SecureString apiKey, String name, Authentication authentication, Set userRoles, + Instant created, Instant expiration, List keyRoles, + Version version) throws IOException { + XContentBuilder builder = XContentFactory.jsonBuilder(); + builder.startObject() + .field("doc_type", "api_key") + .field("creation_time", created.toEpochMilli()) + .field("expiration_time", expiration == null ? null : expiration.toEpochMilli()) + .field("api_key_invalidated", false); + + byte[] utf8Bytes = null; + final char[] keyHash = hasher.hash(apiKey); + try { + utf8Bytes = CharArrays.toUtf8Bytes(keyHash); + builder.field("api_key_hash").utf8Value(utf8Bytes, 0, utf8Bytes.length); } finally { + if (utf8Bytes != null) { + Arrays.fill(utf8Bytes, (byte) 0); + } Arrays.fill(keyHash, (char) 0); } + + + // Save role_descriptors + builder.startObject("role_descriptors"); + if (keyRoles != null && keyRoles.isEmpty() == false) { + for (RoleDescriptor descriptor : keyRoles) { + builder.field(descriptor.getName(), + (contentBuilder, params) -> descriptor.toXContent(contentBuilder, params, true)); + } + } + builder.endObject(); + + // Save limited_by_role_descriptors + builder.startObject("limited_by_role_descriptors"); + for (RoleDescriptor descriptor : userRoles) { + builder.field(descriptor.getName(), + (contentBuilder, params) -> descriptor.toXContent(contentBuilder, params, true)); + } + builder.endObject(); + + builder.field("name", name) + .field("version", version.id) + .startObject("creator") + .field("principal", authentication.getUser().principal()) + .field("metadata", authentication.getUser().metadata()) + .field("realm", authentication.getLookedUpBy() == null ? + authentication.getAuthenticatedBy().getName() : authentication.getLookedUpBy().getName()) + .endObject() + .endObject(); + + return builder; } /** @@ -308,7 +323,7 @@ public class ApiKeyService { * {@code ApiKey }. If found this will attempt to authenticate the key. */ void authenticateWithApiKeyIfPresent(ThreadContext ctx, ActionListener listener) { - if (enabled) { + if (isEnabled()) { final ApiKeyCredentials credentials; try { credentials = getCredentialsFromHeader(ctx); @@ -570,7 +585,14 @@ public class ApiKeyService { } } + private boolean isEnabled() { + return enabled && licenseState.isApiKeyServiceAllowed(); + } + private void ensureEnabled() { + if (licenseState.isApiKeyServiceAllowed() == false) { + throw LicenseUtils.newComplianceException("api keys"); + } if (enabled == false) { throw new IllegalStateException("api keys are not enabled"); } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java index b5d6f35337c..50e98cd0ca6 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/TokenService.java @@ -74,6 +74,8 @@ import org.elasticsearch.index.engine.VersionConflictEngineException; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilders; import org.elasticsearch.index.seqno.SequenceNumbers; +import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.RestStatus; import org.elasticsearch.search.SearchHit; import org.elasticsearch.xpack.core.XPackField; @@ -198,6 +200,7 @@ public final class TokenService { private final SecurityIndexManager securityTokensIndex; private final ExpiredTokenRemover expiredTokenRemover; private final boolean enabled; + private final XPackLicenseState licenseState; private volatile TokenKeys keyCache; private volatile long lastExpirationRunMs; private final AtomicLong createdTimeStamps = new AtomicLong(-1); @@ -205,8 +208,9 @@ public final class TokenService { /** * Creates a new token service */ - public TokenService(Settings settings, Clock clock, Client client, SecurityIndexManager securityMainIndex, - SecurityIndexManager securityTokensIndex, ClusterService clusterService) throws GeneralSecurityException { + public TokenService(Settings settings, Clock clock, Client client, XPackLicenseState licenseState, + SecurityIndexManager securityMainIndex, SecurityIndexManager securityTokensIndex, + ClusterService clusterService) throws GeneralSecurityException { byte[] saltArr = new byte[SALT_BYTES]; secureRandom.nextBytes(saltArr); final SecureString tokenPassphrase = generateTokenKey(); @@ -214,6 +218,7 @@ public final class TokenService { this.clock = clock.withZone(ZoneOffset.UTC); this.expirationDelay = TOKEN_EXPIRATION.get(settings); this.client = client; + this.licenseState = licenseState; this.securityMainIndex = securityMainIndex; this.securityTokensIndex = securityTokensIndex; this.lastExpirationRunMs = client.threadPool().relativeTimeInMillis(); @@ -302,7 +307,7 @@ public final class TokenService { * has not been revoked or is expired. */ void getAndValidateToken(ThreadContext ctx, ActionListener listener) { - if (enabled) { + if (isEnabled()) { final String token = getFromHeader(ctx); if (token == null) { listener.onResponse(null); @@ -1360,9 +1365,16 @@ public final class TokenService { } } + private boolean isEnabled() { + return enabled && licenseState.isTokenServiceAllowed(); + } + private void ensureEnabled() { + if (licenseState.isTokenServiceAllowed() == false) { + throw LicenseUtils.newComplianceException("security tokens"); + } if (enabled == false) { - throw new IllegalStateException("tokens are not enabled"); + throw new IllegalStateException("security tokens are not enabled"); } } diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandler.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandler.java index c2066996f9c..801902d5b96 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandler.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandler.java @@ -71,8 +71,9 @@ public abstract class SecurityBaseRestHandler extends BaseRestHandler { return new IllegalStateException("Security is not enabled but a security rest handler is registered"); } else if (licenseState.isSecurityAvailable() == false) { return LicenseUtils.newComplianceException(XPackField.SECURITY); - } else if (licenseState.isSecurityDisabledByTrialLicense()) { - return new ElasticsearchException("Security must be explicitly enabled when using a trial license. " + + } else if (licenseState.isSecurityDisabledByLicenseDefaults()) { + return new ElasticsearchException("Security must be explicitly enabled when using a [" + + licenseState.getOperationMode().description() + "] license. " + "Enable security by setting [xpack.security.enabled] to [true] in the elasticsearch.yml file " + "and restart the node."); } else { diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/ApiKeyBaseRestHandler.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/ApiKeyBaseRestHandler.java new file mode 100644 index 00000000000..4d797bd543d --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/ApiKeyBaseRestHandler.java @@ -0,0 +1,35 @@ +/* + * 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.xpack.security.rest.action.apikey; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; + +/** + * A base rest handler that handles licensing for ApiKey actions + */ +abstract class ApiKeyBaseRestHandler extends SecurityBaseRestHandler { + ApiKeyBaseRestHandler(Settings settings, XPackLicenseState licenseState) { + super(settings, licenseState); + } + + @Override + protected Exception checkFeatureAvailable(RestRequest request) { + Exception failedFeature = super.checkFeatureAvailable(request); + if (failedFeature != null) { + return failedFeature; + } else if (licenseState.isApiKeyServiceAllowed()) { + return null; + } else { + logger.info("API Keys are not available under the current [{}] license", licenseState.getOperationMode().description()); + return LicenseUtils.newComplianceException("api keys"); + } + } +} diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestCreateApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyAction.java similarity index 94% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestCreateApiKeyAction.java rename to x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyAction.java index 2e3ced0d893..14d4726553d 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestCreateApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyAction.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.security.rest.action; +package org.elasticsearch.xpack.security.rest.action.apikey; import org.elasticsearch.action.support.WriteRequest; import org.elasticsearch.client.node.NodeClient; @@ -24,7 +24,7 @@ import java.io.IOException; /** * Rest action to create an API key */ -public final class RestCreateApiKeyAction extends SecurityBaseRestHandler { +public final class RestCreateApiKeyAction extends ApiKeyBaseRestHandler { /** * @param settings the node's settings diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestGetApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyAction.java similarity index 95% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestGetApiKeyAction.java rename to x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyAction.java index ec0bd7bd9fd..71ed5a06efb 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestGetApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyAction.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.security.rest.action; +package org.elasticsearch.xpack.security.rest.action.apikey; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.Strings; @@ -26,7 +26,7 @@ import java.io.IOException; /** * Rest action to get one or more API keys information. */ -public final class RestGetApiKeyAction extends SecurityBaseRestHandler { +public final class RestGetApiKeyAction extends ApiKeyBaseRestHandler { public RestGetApiKeyAction(Settings settings, RestController controller, XPackLicenseState licenseState) { super(settings, licenseState); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateApiKeyAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestInvalidateApiKeyAction.java similarity index 95% rename from x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateApiKeyAction.java rename to x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestInvalidateApiKeyAction.java index eb10ec6669e..b11a0edde42 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateApiKeyAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/apikey/RestInvalidateApiKeyAction.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.security.rest.action; +package org.elasticsearch.xpack.security.rest.action.apikey; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.ParseField; @@ -28,7 +28,7 @@ import java.io.IOException; /** * Rest action to invalidate one or more API keys */ -public final class RestInvalidateApiKeyAction extends SecurityBaseRestHandler { +public final class RestInvalidateApiKeyAction extends ApiKeyBaseRestHandler { static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("invalidate_api_key", a -> { return new InvalidateApiKeyRequest((String) a[0], (String) a[1], (String) a[2], (String) a[3]); diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oauth2/RestGetTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oauth2/RestGetTokenAction.java index 94317145b02..4734c39bc5a 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oauth2/RestGetTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oauth2/RestGetTokenAction.java @@ -30,7 +30,6 @@ import org.elasticsearch.xpack.core.security.action.token.CreateTokenAction; import org.elasticsearch.xpack.core.security.action.token.CreateTokenRequest; import org.elasticsearch.xpack.core.security.action.token.CreateTokenResponse; import org.elasticsearch.xpack.core.security.action.token.RefreshTokenAction; -import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; import java.io.IOException; import java.util.Arrays; @@ -45,7 +44,7 @@ import static org.elasticsearch.rest.RestRequest.Method.POST; * specification as this aspect does not make the most sense since the response body is * expected to be JSON */ -public final class RestGetTokenAction extends SecurityBaseRestHandler { +public final class RestGetTokenAction extends TokenBaseRestHandler { private static final DeprecationLogger deprecationLogger = new DeprecationLogger(LogManager.getLogger(RestGetTokenAction.class)); static final ConstructingObjectParser PARSER = new ConstructingObjectParser<>("token_request", diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oauth2/RestInvalidateTokenAction.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oauth2/RestInvalidateTokenAction.java index 9801f3c93c8..cb72e53bcc8 100644 --- a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oauth2/RestInvalidateTokenAction.java +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oauth2/RestInvalidateTokenAction.java @@ -24,7 +24,6 @@ import org.elasticsearch.rest.action.RestBuilderListener; import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenAction; import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenRequest; import org.elasticsearch.xpack.core.security.action.token.InvalidateTokenResponse; -import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; import java.io.IOException; @@ -33,7 +32,7 @@ import static org.elasticsearch.rest.RestRequest.Method.DELETE; /** * Rest handler for handling access token invalidation requests */ -public final class RestInvalidateTokenAction extends SecurityBaseRestHandler { +public final class RestInvalidateTokenAction extends TokenBaseRestHandler { private static final DeprecationLogger deprecationLogger = new DeprecationLogger(LogManager.getLogger(RestInvalidateTokenAction.class)); static final ConstructingObjectParser PARSER = diff --git a/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oauth2/TokenBaseRestHandler.java b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oauth2/TokenBaseRestHandler.java new file mode 100644 index 00000000000..7111a5387fe --- /dev/null +++ b/x-pack/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/oauth2/TokenBaseRestHandler.java @@ -0,0 +1,40 @@ +/* + * 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.xpack.security.rest.action.oauth2; + +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.LicenseUtils; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; + +/** + * A base rest handler that handles licensing for Token actions + */ +abstract class TokenBaseRestHandler extends SecurityBaseRestHandler { + + protected Logger logger = LogManager.getLogger(getClass()); + + TokenBaseRestHandler(Settings settings, XPackLicenseState licenseState) { + super(settings, licenseState); + } + + @Override + protected Exception checkFeatureAvailable(RestRequest request) { + Exception failedFeature = super.checkFeatureAvailable(request); + if (failedFeature != null) { + return failedFeature; + } else if (licenseState.isTokenServiceAllowed()) { + return null; + } else { + logger.info("Security tokens are not available under the current [{}] license", licenseState.getOperationMode().description()); + return LicenseUtils.newComplianceException("security tokens"); + } + } +} diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/license/LicensingTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/license/LicensingTests.java index 23bcef624ac..1bf5d48e519 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/license/LicensingTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/license/LicensingTests.java @@ -133,7 +133,7 @@ public class LicensingTests extends SecurityIntegTestCase { @Before public void resetLicensing() throws InterruptedException { - enableLicensing(OperationMode.BASIC); + enableLicensing(OperationMode.MISSING); } @After @@ -230,7 +230,7 @@ public class LicensingTests extends SecurityIntegTestCase { // enable a license that enables security License.OperationMode mode = randomFrom(License.OperationMode.GOLD, License.OperationMode.TRIAL, - License.OperationMode.PLATINUM, License.OperationMode.STANDARD); + License.OperationMode.PLATINUM, License.OperationMode.STANDARD, OperationMode.BASIC); enableLicensing(mode); // security actions should work! try (TransportClient client = new TestXPackTransportClient(settings, LocalStateSecurity.class)) { diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityFeatureSetTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityFeatureSetTests.java index d3c533891a8..c9d8c68347a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityFeatureSetTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/SecurityFeatureSetTests.java @@ -81,7 +81,7 @@ public class SecurityFeatureSetTests extends ESTestCase { rolesStore, roleMappingStore, ipFilter); assertThat(featureSet.enabled(), is(true)); - when(licenseState.isSecurityDisabledByTrialLicense()).thenReturn(true); + when(licenseState.isSecurityDisabledByLicenseDefaults()).thenReturn(true); featureSet = new SecurityFeatureSet(settings, licenseState, realms, rolesStore, roleMappingStore, ipFilter); assertThat(featureSet.enabled(), is(false)); @@ -245,7 +245,7 @@ public class SecurityFeatureSetTests extends ESTestCase { public void testUsageOnTrialLicenseWithSecurityDisabledByDefault() throws Exception { when(licenseState.isSecurityAvailable()).thenReturn(true); - when(licenseState.isSecurityDisabledByTrialLicense()).thenReturn(true); + when(licenseState.isSecurityDisabledByLicenseDefaults()).thenReturn(true); Settings.Builder settings = Settings.builder().put(this.settings); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectLogoutActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectLogoutActionTests.java index e31ccc67332..2505f55c98d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectLogoutActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/oidc/TransportOpenIdConnectLogoutActionTests.java @@ -30,6 +30,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.tasks.Task; import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.threadpool.ThreadPool; @@ -167,7 +168,12 @@ public class TransportOpenIdConnectLogoutActionTests extends OpenIdConnectTestCa when(securityIndex.isAvailable()).thenReturn(true); final ClusterService clusterService = ClusterServiceUtils.createClusterService(threadPool); - tokenService = new TokenService(settings, Clock.systemUTC(), client, securityIndex, securityIndex, clusterService); + + final XPackLicenseState licenseState = mock(XPackLicenseState.class); + when(licenseState.isTokenServiceAllowed()).thenReturn(true); + + tokenService = new TokenService(settings, Clock.systemUTC(), client, licenseState, + securityIndex, securityIndex, clusterService); final TransportService transportService = new TransportService(Settings.EMPTY, mock(Transport.class), null, TransportService.NOOP_TRANSPORT_INTERCEPTOR, x -> null, null, Collections.emptySet()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionActionTests.java index 63c58c5ce10..3f4ac894208 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlInvalidateSessionActionTests.java @@ -45,6 +45,7 @@ import org.elasticsearch.env.TestEnvironment; import org.elasticsearch.index.query.BoolQueryBuilder; import org.elasticsearch.index.query.QueryBuilder; import org.elasticsearch.index.query.TermQueryBuilder; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.search.SearchHit; import org.elasticsearch.search.SearchHits; import org.elasticsearch.tasks.Task; @@ -200,8 +201,11 @@ public class TransportSamlInvalidateSessionActionTests extends SamlTestCase { when(securityIndex.aliasName()).thenReturn(".security"); when(securityIndex.freeze()).thenReturn(securityIndex); + final XPackLicenseState licenseState = mock(XPackLicenseState.class); + when(licenseState.isTokenServiceAllowed()).thenReturn(true); + final ClusterService clusterService = ClusterServiceUtils.createClusterService(threadPool); - tokenService = new TokenService(settings, Clock.systemUTC(), client, securityIndex, securityIndex, clusterService); + tokenService = new TokenService(settings, Clock.systemUTC(), client, licenseState, securityIndex, securityIndex, clusterService); final TransportService transportService = new TransportService(Settings.EMPTY, mock(Transport.class), null, TransportService.NOOP_TRANSPORT_INTERCEPTOR, x -> null, null, Collections.emptySet()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutActionTests.java index bd1a20db2f1..1652122bf6e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/saml/TransportSamlLogoutActionTests.java @@ -37,6 +37,7 @@ import org.elasticsearch.common.settings.Settings; import org.elasticsearch.common.util.concurrent.ThreadContext; import org.elasticsearch.env.Environment; import org.elasticsearch.env.TestEnvironment; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.tasks.Task; import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.threadpool.ThreadPool; @@ -202,8 +203,10 @@ public class TransportSamlLogoutActionTests extends SamlTestCase { }).when(securityIndex).checkIndexVersionThenExecute(any(Consumer.class), any(Runnable.class)); when(securityIndex.isAvailable()).thenReturn(true); + final XPackLicenseState licenseState = mock(XPackLicenseState.class); + when(licenseState.isTokenServiceAllowed()).thenReturn(true); final ClusterService clusterService = ClusterServiceUtils.createClusterService(threadPool); - tokenService = new TokenService(settings, Clock.systemUTC(), client, securityIndex, securityIndex, clusterService); + tokenService = new TokenService(settings, Clock.systemUTC(), client, licenseState, securityIndex, securityIndex, clusterService); final TransportService transportService = new TransportService(Settings.EMPTY, mock(Transport.class), null, TransportService.NOOP_TRANSPORT_INTERCEPTOR, x -> null, null, Collections.emptySet()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenActionTests.java index cea1a532e13..c2050c95fc3 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/action/token/TransportCreateTokenActionTests.java @@ -29,6 +29,7 @@ import org.elasticsearch.common.UUIDs; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.node.Node; import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.test.ESTestCase; @@ -72,6 +73,7 @@ public class TransportCreateTokenActionTests extends ESTestCase { private ClusterService clusterService; private AtomicReference idxReqReference; private AuthenticationService authenticationService; + private XPackLicenseState license; @Before public void setupClient() { @@ -137,6 +139,9 @@ public class TransportCreateTokenActionTests extends ESTestCase { any(UsernamePasswordToken.class), any(ActionListener.class)); this.clusterService = ClusterServiceUtils.createClusterService(threadPool); + + this.license = mock(XPackLicenseState.class); + when(license.isTokenServiceAllowed()).thenReturn(true); } @After @@ -147,8 +152,8 @@ public class TransportCreateTokenActionTests extends ESTestCase { } public void testClientCredentialsCreatesWithoutRefreshToken() throws Exception { - final TokenService tokenService = new TokenService(SETTINGS, Clock.systemUTC(), client, securityIndex, securityIndex, - clusterService); + final TokenService tokenService = new TokenService(SETTINGS, Clock.systemUTC(), client, license, + securityIndex, securityIndex, clusterService); Authentication authentication = new Authentication(new User("joe"), new Authentication.RealmRef("realm", "type", "node"), null); authentication.writeToContext(threadPool.getThreadContext()); @@ -172,8 +177,8 @@ public class TransportCreateTokenActionTests extends ESTestCase { } public void testPasswordGrantTypeCreatesWithRefreshToken() throws Exception { - final TokenService tokenService = new TokenService(SETTINGS, Clock.systemUTC(), client, securityIndex, securityIndex, - clusterService); + final TokenService tokenService = new TokenService(SETTINGS, Clock.systemUTC(), client, license, + securityIndex, securityIndex, clusterService); Authentication authentication = new Authentication(new User("joe"), new Authentication.RealmRef("realm", "type", "node"), null); authentication.writeToContext(threadPool.getThreadContext()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java index 507cb8349aa..0491d20d74c 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/ApiKeyServiceTests.java @@ -9,6 +9,7 @@ package org.elasticsearch.xpack.security.authc; import org.elasticsearch.Version; import org.elasticsearch.action.ActionListener; import org.elasticsearch.action.support.PlainActionFuture; +import org.elasticsearch.client.Client; import org.elasticsearch.common.bytes.BytesReference; import org.elasticsearch.common.settings.SecureString; import org.elasticsearch.common.settings.Settings; @@ -18,10 +19,12 @@ import org.elasticsearch.common.xcontent.XContentBuilder; import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.common.xcontent.json.JsonXContent; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.test.ESTestCase; import org.elasticsearch.threadpool.TestThreadPool; import org.elasticsearch.threadpool.ThreadPool; +import org.elasticsearch.xpack.core.XPackSettings; import org.elasticsearch.xpack.core.security.authc.Authentication; import org.elasticsearch.xpack.core.security.authc.Authentication.AuthenticationType; import org.elasticsearch.xpack.core.security.authc.Authentication.RealmRef; @@ -29,17 +32,20 @@ import org.elasticsearch.xpack.core.security.authc.AuthenticationResult; import org.elasticsearch.xpack.core.security.authc.support.Hasher; import org.elasticsearch.xpack.core.security.authz.RoleDescriptor; import org.elasticsearch.xpack.core.security.authz.privilege.ApplicationPrivilege; -import org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore; import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.security.authc.ApiKeyService.ApiKeyCredentials; import org.elasticsearch.xpack.security.authc.ApiKeyService.ApiKeyRoleDescriptors; import org.elasticsearch.xpack.security.authc.ApiKeyService.CachedApiKeyHashResult; import org.elasticsearch.xpack.security.authz.store.NativePrivilegeStore; +import org.elasticsearch.xpack.security.support.SecurityIndexManager; +import org.elasticsearch.xpack.security.test.SecurityMocks; import org.junit.After; import org.junit.Before; +import java.io.IOException; import java.nio.charset.StandardCharsets; import java.time.Clock; +import java.time.Instant; import java.time.temporal.ChronoUnit; import java.util.Arrays; import java.util.Base64; @@ -48,19 +54,26 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import static org.elasticsearch.xpack.core.security.authz.store.ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR; import static org.hamcrest.Matchers.arrayContaining; import static org.hamcrest.Matchers.equalTo; import static org.hamcrest.Matchers.instanceOf; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.not; +import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.sameInstance; import static org.mockito.Matchers.any; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; public class ApiKeyServiceTests extends ESTestCase { private ThreadPool threadPool; + private XPackLicenseState licenseState; + private Client client; + private SecurityIndexManager securityIndex; @Before public void createThreadPool() { @@ -72,6 +85,15 @@ public class ApiKeyServiceTests extends ESTestCase { terminate(threadPool); } + @Before + public void setupMocks() { + this.licenseState = mock(XPackLicenseState.class); + when(licenseState.isApiKeyServiceAllowed()).thenReturn(true); + + this.client = mock(Client.class); + this.securityIndex = SecurityMocks.mockSecurityIndexManager(); + } + public void testGetCredentialsFromThreadContext() { ThreadContext threadContext = threadPool.getThreadContext(); assertNull(ApiKeyService.getCredentialsFromHeader(threadContext)); @@ -107,6 +129,57 @@ public class ApiKeyServiceTests extends ESTestCase { } } + public void testAuthenticateWithApiKey() throws Exception { + final Settings settings = Settings.builder().put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true).build(); + final ApiKeyService service = createApiKeyService(settings); + + final String id = randomAlphaOfLength(12); + final String key = randomAlphaOfLength(16); + + mockKeyDocument(service, id, key, new User("hulk", "superuser")); + + final AuthenticationResult auth = tryAuthenticate(service, id, key); + assertThat(auth.getStatus(), is(AuthenticationResult.Status.SUCCESS)); + assertThat(auth.getUser(), notNullValue()); + assertThat(auth.getUser().principal(), is("hulk")); + } + + public void testAuthenticationIsSkippedIfLicenseDoesNotAllowIt() throws Exception { + final Settings settings = Settings.builder().put(XPackSettings.API_KEY_SERVICE_ENABLED_SETTING.getKey(), true).build(); + final ApiKeyService service = createApiKeyService(settings); + + final String id = randomAlphaOfLength(12); + final String key = randomAlphaOfLength(16); + + mockKeyDocument(service, id, key, new User(randomAlphaOfLength(6), randomAlphaOfLength(12))); + + when(licenseState.isApiKeyServiceAllowed()).thenReturn(false); + final AuthenticationResult auth = tryAuthenticate(service, id, key); + assertThat(auth.getStatus(), is(AuthenticationResult.Status.CONTINUE)); + assertThat(auth.getUser(), nullValue()); + } + + public void mockKeyDocument(ApiKeyService service, String id, String key, User user) throws IOException { + final Authentication authentication = new Authentication(user, new RealmRef("realm1", "native", "node01"), null, Version.CURRENT); + final XContentBuilder docSource = service.newDocument(new SecureString(key.toCharArray()), "test", authentication, + Collections.singleton(SUPERUSER_ROLE_DESCRIPTOR), Instant.now(), Instant.now().plusSeconds(3600), null, Version.CURRENT); + + SecurityMocks.mockGetRequest(client, id, BytesReference.bytes(docSource)); + } + + private AuthenticationResult tryAuthenticate(ApiKeyService service, String id, String key) throws Exception { + final ThreadContext threadContext = threadPool.getThreadContext(); + final String header = "ApiKey " + Base64.getEncoder().encodeToString((id + ":" + key).getBytes(StandardCharsets.UTF_8)); + threadContext.putHeader("Authorization", header); + + final PlainActionFuture future = new PlainActionFuture<>(); + service.authenticateWithApiKeyIfPresent(threadContext, future); + + final AuthenticationResult auth = future.get(); + assertThat(auth, notNullValue()); + return auth; + } + public void testValidateApiKey() throws Exception { final String apiKey = randomAlphaOfLength(16); Hasher hasher = randomFrom(Hasher.PBKDF2, Hasher.BCRYPT4, Hasher.BCRYPT); @@ -122,8 +195,7 @@ public class ApiKeyServiceTests extends ESTestCase { sourceMap.put("creator", creatorMap); sourceMap.put("api_key_invalidated", false); - ApiKeyService service = new ApiKeyService(Settings.EMPTY, Clock.systemUTC(), null, null, - ClusterServiceUtils.createClusterService(threadPool), threadPool); + ApiKeyService service = createApiKeyService(Settings.EMPTY); ApiKeyService.ApiKeyCredentials creds = new ApiKeyService.ApiKeyCredentials(randomAlphaOfLength(12), new SecureString(apiKey.toCharArray())); PlainActionFuture future = new PlainActionFuture<>(); @@ -136,7 +208,7 @@ public class ApiKeyServiceTests extends ESTestCase { assertThat(result.getUser().metadata(), is(Collections.emptyMap())); assertThat(result.getMetadata().get(ApiKeyService.API_KEY_ROLE_DESCRIPTORS_KEY), equalTo(sourceMap.get("role_descriptors"))); assertThat(result.getMetadata().get(ApiKeyService.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY), - equalTo(sourceMap.get("limited_by_role_descriptors"))); + equalTo(sourceMap.get("limited_by_role_descriptors"))); sourceMap.put("expiration_time", Clock.systemUTC().instant().plus(1L, ChronoUnit.HOURS).toEpochMilli()); future = new PlainActionFuture<>(); @@ -149,7 +221,7 @@ public class ApiKeyServiceTests extends ESTestCase { assertThat(result.getUser().metadata(), is(Collections.emptyMap())); assertThat(result.getMetadata().get(ApiKeyService.API_KEY_ROLE_DESCRIPTORS_KEY), equalTo(sourceMap.get("role_descriptors"))); assertThat(result.getMetadata().get(ApiKeyService.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY), - equalTo(sourceMap.get("limited_by_role_descriptors"))); + equalTo(sourceMap.get("limited_by_role_descriptors"))); sourceMap.put("expiration_time", Clock.systemUTC().instant().minus(1L, ChronoUnit.HOURS).toEpochMilli()); future = new PlainActionFuture<>(); @@ -165,7 +237,7 @@ public class ApiKeyServiceTests extends ESTestCase { result = future.get(); assertNotNull(result); assertFalse(result.isAuthenticated()); - + sourceMap.put("api_key_invalidated", true); creds = new ApiKeyService.ApiKeyCredentials(randomAlphaOfLength(12), new SecureString(randomAlphaOfLength(15).toCharArray())); future = new PlainActionFuture<>(); @@ -179,7 +251,7 @@ public class ApiKeyServiceTests extends ESTestCase { Map superUserRdMap; try (XContentBuilder builder = JsonXContent.contentBuilder()) { superUserRdMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), - BytesReference.bytes(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR + BytesReference.bytes(SUPERUSER_ROLE_DESCRIPTOR .toXContent(builder, ToXContent.EMPTY_PARAMS, true)) .streamInput(), false); @@ -187,14 +259,13 @@ public class ApiKeyServiceTests extends ESTestCase { Map authMetadata = new HashMap<>(); authMetadata.put(ApiKeyService.API_KEY_ID_KEY, randomAlphaOfLength(12)); authMetadata.put(ApiKeyService.API_KEY_ROLE_DESCRIPTORS_KEY, - Collections.singletonMap(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName(), superUserRdMap)); + Collections.singletonMap(SUPERUSER_ROLE_DESCRIPTOR.getName(), superUserRdMap)); authMetadata.put(ApiKeyService.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, - Collections.singletonMap(ReservedRolesStore.SUPERUSER_ROLE_DESCRIPTOR.getName(), superUserRdMap)); + Collections.singletonMap(SUPERUSER_ROLE_DESCRIPTOR.getName(), superUserRdMap)); final Authentication authentication = new Authentication(new User("joe"), new RealmRef("apikey", "apikey", "node"), null, Version.CURRENT, AuthenticationType.API_KEY, authMetadata); - ApiKeyService service = new ApiKeyService(Settings.EMPTY, Clock.systemUTC(), null, null, - ClusterServiceUtils.createClusterService(threadPool), threadPool); + ApiKeyService service = createApiKeyService(Settings.EMPTY); PlainActionFuture roleFuture = new PlainActionFuture<>(); service.getRoleForApiKey(authentication, roleFuture); @@ -208,22 +279,22 @@ public class ApiKeyServiceTests extends ESTestCase { authMetadata.put(ApiKeyService.API_KEY_ID_KEY, randomAlphaOfLength(12)); boolean emptyApiKeyRoleDescriptor = randomBoolean(); final RoleDescriptor roleARoleDescriptor = new RoleDescriptor("a role", new String[] { "monitor" }, - new RoleDescriptor.IndicesPrivileges[] { - RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("monitor").build() }, - null); + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("monitor").build() }, + null); Map roleARDMap; try (XContentBuilder builder = JsonXContent.contentBuilder()) { roleARDMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), - BytesReference.bytes(roleARoleDescriptor.toXContent(builder, ToXContent.EMPTY_PARAMS, true)).streamInput(), false); + BytesReference.bytes(roleARoleDescriptor.toXContent(builder, ToXContent.EMPTY_PARAMS, true)).streamInput(), false); } authMetadata.put(ApiKeyService.API_KEY_ROLE_DESCRIPTORS_KEY, - (emptyApiKeyRoleDescriptor) ? randomFrom(Arrays.asList(null, Collections.emptyMap())) - : Collections.singletonMap("a role", roleARDMap)); + (emptyApiKeyRoleDescriptor) ? randomFrom(Arrays.asList(null, Collections.emptyMap())) + : Collections.singletonMap("a role", roleARDMap)); final RoleDescriptor limitedRoleDescriptor = new RoleDescriptor("limited role", new String[] { "all" }, - new RoleDescriptor.IndicesPrivileges[] { - RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("all").build() }, - null); + new RoleDescriptor.IndicesPrivileges[] { + RoleDescriptor.IndicesPrivileges.builder().indices("*").privileges("all").build() }, + null); Map limitedRdMap; try (XContentBuilder builder = JsonXContent.contentBuilder()) { limitedRdMap = XContentHelper.convertToMap(XContentType.JSON.xContent(), @@ -235,7 +306,7 @@ public class ApiKeyServiceTests extends ESTestCase { authMetadata.put(ApiKeyService.API_KEY_LIMITED_ROLE_DESCRIPTORS_KEY, Collections.singletonMap("limited role", limitedRdMap)); final Authentication authentication = new Authentication(new User("joe"), new RealmRef("apikey", "apikey", "node"), null, - Version.CURRENT, AuthenticationType.API_KEY, authMetadata); + Version.CURRENT, AuthenticationType.API_KEY, authMetadata); final NativePrivilegeStore privilegesStore = mock(NativePrivilegeStore.class); doAnswer(i -> { @@ -247,8 +318,7 @@ public class ApiKeyServiceTests extends ESTestCase { return null; } ).when(privilegesStore).getPrivileges(any(Collection.class), any(Collection.class), any(ActionListener.class)); - ApiKeyService service = new ApiKeyService(Settings.EMPTY, Clock.systemUTC(), null, null, - ClusterServiceUtils.createClusterService(threadPool), threadPool); + ApiKeyService service = createApiKeyService(Settings.EMPTY); PlainActionFuture roleFuture = new PlainActionFuture<>(); service.getRoleForApiKey(authentication, roleFuture); @@ -280,8 +350,7 @@ public class ApiKeyServiceTests extends ESTestCase { sourceMap.put("creator", creatorMap); sourceMap.put("api_key_invalidated", false); - ApiKeyService service = new ApiKeyService(Settings.EMPTY, Clock.systemUTC(), null, null, - ClusterServiceUtils.createClusterService(threadPool), threadPool); + ApiKeyService service = createApiKeyService(Settings.EMPTY); ApiKeyCredentials creds = new ApiKeyCredentials(randomAlphaOfLength(12), new SecureString(apiKey.toCharArray())); PlainActionFuture future = new PlainActionFuture<>(); service.validateApiKeyCredentials(sourceMap, creds, Clock.systemUTC(), future); @@ -345,8 +414,7 @@ public class ApiKeyServiceTests extends ESTestCase { sourceMap.put("creator", creatorMap); sourceMap.put("api_key_invalidated", false); - ApiKeyService service = new ApiKeyService(settings, Clock.systemUTC(), null, null, - ClusterServiceUtils.createClusterService(threadPool), threadPool); + ApiKeyService service = createApiKeyService(settings); ApiKeyCredentials creds = new ApiKeyCredentials(randomAlphaOfLength(12), new SecureString(apiKey.toCharArray())); PlainActionFuture future = new PlainActionFuture<>(); service.validateApiKeyCredentials(sourceMap, creds, Clock.systemUTC(), future); @@ -355,4 +423,11 @@ public class ApiKeyServiceTests extends ESTestCase { CachedApiKeyHashResult cachedApiKeyHashResult = service.getFromCache(creds.getId()); assertNull(cachedApiKeyHashResult); } + + private ApiKeyService createApiKeyService(Settings settings) { + return new ApiKeyService(settings, Clock.systemUTC(), client, licenseState, securityIndex, + ClusterServiceUtils.createClusterService(threadPool), threadPool); + } + + } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java index a86edb98251..b7df9193e34 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/AuthenticationServiceTests.java @@ -179,6 +179,8 @@ public class AuthenticationServiceTests extends ESTestCase { XPackLicenseState licenseState = mock(XPackLicenseState.class); when(licenseState.allowedRealmType()).thenReturn(XPackLicenseState.AllowedRealmType.ALL); when(licenseState.isAuthAllowed()).thenReturn(true); + when(licenseState.isApiKeyServiceAllowed()).thenReturn(true); + when(licenseState.isTokenServiceAllowed()).thenReturn(true); realms = spy(new TestRealms(Settings.EMPTY, TestEnvironment.newEnvironment(settings), Collections.emptyMap(), licenseState, threadContext, mock(ReservedRealm.class), Arrays.asList(firstRealm, secondRealm), Collections.singletonList(firstRealm))); @@ -219,8 +221,13 @@ public class AuthenticationServiceTests extends ESTestCase { return null; }).when(securityIndex).checkIndexVersionThenExecute(any(Consumer.class), any(Runnable.class)); ClusterService clusterService = ClusterServiceUtils.createClusterService(threadPool); +<<<<<<< HEAD apiKeyService = new ApiKeyService(settings, Clock.systemUTC(), client, securityIndex, clusterService, threadPool); tokenService = new TokenService(settings, Clock.systemUTC(), client, securityIndex, securityIndex, clusterService); +======= + apiKeyService = new ApiKeyService(settings, Clock.systemUTC(), client, licenseState, securityIndex, clusterService, threadPool); + tokenService = new TokenService(settings, Clock.systemUTC(), client, licenseState, securityIndex, clusterService); +>>>>>>> Security on Basic License (#107) service = new AuthenticationService(settings, realms, auditTrail, new DefaultAuthenticationFailureHandler(Collections.emptyMap()), threadPool, new AnonymousUser(settings), tokenService, apiKeyService); } diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java index 494b8070c57..44ca52a2a6a 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/authc/TokenServiceTests.java @@ -39,6 +39,7 @@ import org.elasticsearch.common.xcontent.XContentHelper; import org.elasticsearch.common.xcontent.XContentType; import org.elasticsearch.index.Index; import org.elasticsearch.index.shard.ShardId; +import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.node.Node; import org.elasticsearch.test.ClusterServiceUtils; import org.elasticsearch.test.ESTestCase; @@ -54,6 +55,8 @@ import org.elasticsearch.xpack.core.security.authc.support.TokensInvalidationRes import org.elasticsearch.xpack.core.security.user.User; import org.elasticsearch.xpack.core.watcher.watch.ClockMock; import org.elasticsearch.xpack.security.support.SecurityIndexManager; +import org.elasticsearch.xpack.security.test.SecurityMocks; +import org.hamcrest.Matchers; import org.junit.After; import org.junit.AfterClass; import org.junit.Before; @@ -71,7 +74,6 @@ import java.util.Base64; import java.util.Collections; import java.util.HashMap; import java.util.Map; -import java.util.function.Consumer; import javax.crypto.CipherOutputStream; import javax.crypto.SecretKey; @@ -103,6 +105,7 @@ public class TokenServiceTests extends ESTestCase { private DiscoveryNode oldNode; private Settings tokenServiceEnabledSettings = Settings.builder() .put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), true).build(); + private XPackLicenseState licenseState; @Before public void setupClient() { @@ -128,9 +131,14 @@ public class TokenServiceTests extends ESTestCase { }).when(client).execute(eq(IndexAction.INSTANCE), any(IndexRequest.class), any(ActionListener.class)); // setup lifecycle service - this.securityMainIndex = mockSecurityManager(); - this.securityTokensIndex = mockSecurityManager(); + this.securityMainIndex = SecurityMocks.mockSecurityIndexManager(); + this.securityTokensIndex = SecurityMocks.mockSecurityIndexManager(); this.clusterService = ClusterServiceUtils.createClusterService(threadPool); + + // License state (enabled by default) + licenseState = mock(XPackLicenseState.class); + when(licenseState.isTokenServiceAllowed()).thenReturn(true); + // version 7.1 was an "inflection" point in the Token Service development (access_tokens as UUIDS, multiple concurrent refreshes, // tokens docs on a separate index), let's test the TokenService works in a mixed cluster with nodes with versions prior to these // developments @@ -159,8 +167,7 @@ public class TokenServiceTests extends ESTestCase { } public void testAttachAndGetToken() throws Exception { - TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityMainIndex, - securityTokensIndex, clusterService); + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); tokenService.createOAuth2Tokens(authentication, authentication, Collections.emptyMap(), true, tokenFuture); @@ -181,8 +188,7 @@ public class TokenServiceTests extends ESTestCase { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { // verify a second separate token service with its own salt can also verify - TokenService anotherService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityMainIndex, - securityTokensIndex, clusterService); + TokenService anotherService = createTokenService(tokenServiceEnabledSettings, systemUTC()); anotherService.refreshMetaData(tokenService.getTokenMetaData()); PlainActionFuture future = new PlainActionFuture<>(); anotherService.getAndValidateToken(requestContext, future); @@ -192,8 +198,7 @@ public class TokenServiceTests extends ESTestCase { } public void testInvalidAuthorizationHeader() throws Exception { - TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityMainIndex, - securityTokensIndex, clusterService); + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); ThreadContext requestContext = new ThreadContext(Settings.EMPTY); String token = randomFrom("", " "); String authScheme = randomFrom("Bearer ", "BEARER ", "bearer ", "Basic "); @@ -208,8 +213,7 @@ public class TokenServiceTests extends ESTestCase { } public void testRotateKey() throws Exception { - TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityMainIndex, - securityTokensIndex, clusterService); + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); tokenService.createOAuth2Tokens(authentication, authentication, Collections.emptyMap(), true, tokenFuture); @@ -219,7 +223,7 @@ public class TokenServiceTests extends ESTestCase { authentication = token.getAuthentication(); ThreadContext requestContext = new ThreadContext(Settings.EMPTY); - requestContext.putHeader("Authorization", "Bearer " + getDeprecatedAccessTokenString(tokenService, token)); + storeTokenHeader(requestContext, getDeprecatedAccessTokenString(tokenService, token)); try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); @@ -243,7 +247,7 @@ public class TokenServiceTests extends ESTestCase { assertNotEquals(getDeprecatedAccessTokenString(tokenService, newToken), getDeprecatedAccessTokenString(tokenService, token)); requestContext = new ThreadContext(Settings.EMPTY); - requestContext.putHeader("Authorization", "Bearer " + getDeprecatedAccessTokenString(tokenService, newToken)); + storeTokenHeader(requestContext, getDeprecatedAccessTokenString(tokenService, newToken)); mockGetTokenFromId(newToken, false); try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { @@ -262,14 +266,13 @@ public class TokenServiceTests extends ESTestCase { } public void testKeyExchange() throws Exception { - TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityMainIndex, - securityTokensIndex, clusterService); + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); int numRotations = randomIntBetween(1, 5); for (int i = 0; i < numRotations; i++) { rotateKeys(tokenService); } - TokenService otherTokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityMainIndex, - securityTokensIndex, clusterService); + TokenService otherTokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); + otherTokenService.refreshMetaData(tokenService.getTokenMetaData()); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); @@ -280,7 +283,7 @@ public class TokenServiceTests extends ESTestCase { authentication = token.getAuthentication(); ThreadContext requestContext = new ThreadContext(Settings.EMPTY); - requestContext.putHeader("Authorization", "Bearer " + getDeprecatedAccessTokenString(tokenService, token)); + storeTokenHeader(requestContext, getDeprecatedAccessTokenString(tokenService, token)); try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); otherTokenService.getAndValidateToken(requestContext, future); @@ -301,8 +304,7 @@ public class TokenServiceTests extends ESTestCase { } public void testPruneKeys() throws Exception { - TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityMainIndex, - securityTokensIndex, clusterService); + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); tokenService.createOAuth2Tokens(authentication, authentication, Collections.emptyMap(), true, tokenFuture); @@ -312,7 +314,7 @@ public class TokenServiceTests extends ESTestCase { authentication = token.getAuthentication(); ThreadContext requestContext = new ThreadContext(Settings.EMPTY); - requestContext.putHeader("Authorization", "Bearer " + getDeprecatedAccessTokenString(tokenService, token)); + storeTokenHeader(requestContext, getDeprecatedAccessTokenString(tokenService, token)); try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); @@ -352,7 +354,7 @@ public class TokenServiceTests extends ESTestCase { } requestContext = new ThreadContext(Settings.EMPTY); - requestContext.putHeader("Authorization", "Bearer " + getDeprecatedAccessTokenString(tokenService, newToken)); + storeTokenHeader(requestContext, getDeprecatedAccessTokenString(tokenService, newToken)); mockGetTokenFromId(newToken, false); try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); @@ -364,8 +366,7 @@ public class TokenServiceTests extends ESTestCase { } public void testPassphraseWorks() throws Exception { - TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityMainIndex, - securityTokensIndex, clusterService); + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); tokenService.createOAuth2Tokens(authentication, authentication, Collections.emptyMap(), true, tokenFuture); @@ -375,7 +376,7 @@ public class TokenServiceTests extends ESTestCase { authentication = token.getAuthentication(); ThreadContext requestContext = new ThreadContext(Settings.EMPTY); - requestContext.putHeader("Authorization", "Bearer " + getDeprecatedAccessTokenString(tokenService, token)); + storeTokenHeader(requestContext, getDeprecatedAccessTokenString(tokenService, token)); try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); @@ -386,8 +387,7 @@ public class TokenServiceTests extends ESTestCase { try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { // verify a second separate token service with its own passphrase cannot verify - TokenService anotherService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityMainIndex, - securityTokensIndex, clusterService); + TokenService anotherService = createTokenService(tokenServiceEnabledSettings, systemUTC()); PlainActionFuture future = new PlainActionFuture<>(); anotherService.getAndValidateToken(requestContext, future); assertNull(future.get()); @@ -395,8 +395,7 @@ public class TokenServiceTests extends ESTestCase { } public void testGetTokenWhenKeyCacheHasExpired() throws Exception { - TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityMainIndex, - securityTokensIndex, clusterService); + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); @@ -410,8 +409,7 @@ public class TokenServiceTests extends ESTestCase { public void testInvalidatedToken() throws Exception { when(securityMainIndex.indexExists()).thenReturn(true); - TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityMainIndex, - securityTokensIndex, clusterService); + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); tokenService.createOAuth2Tokens(authentication, authentication, Collections.emptyMap(), true, tokenFuture); @@ -420,7 +418,7 @@ public class TokenServiceTests extends ESTestCase { mockGetTokenFromId(token, true); ThreadContext requestContext = new ThreadContext(Settings.EMPTY); - requestContext.putHeader("Authorization", "Bearer " + tokenService.getAccessTokenAsString(token)); + storeTokenHeader(requestContext, tokenService.getAccessTokenAsString(token)); try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); @@ -432,6 +430,10 @@ public class TokenServiceTests extends ESTestCase { } } + private void storeTokenHeader(ThreadContext requestContext, String tokenString) throws IOException, GeneralSecurityException { + requestContext.putHeader("Authorization", "Bearer " + tokenString); + } + public void testComputeSecretKeyIsConsistent() throws Exception { byte[] saltArr = new byte[32]; random().nextBytes(saltArr); @@ -465,8 +467,7 @@ public class TokenServiceTests extends ESTestCase { public void testTokenExpiry() throws Exception { ClockMock clock = ClockMock.frozen(); - TokenService tokenService = new TokenService(tokenServiceEnabledSettings, clock, client, securityMainIndex, securityTokensIndex, - clusterService); + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, clock); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); tokenService.createOAuth2Tokens(authentication, authentication, Collections.emptyMap(), true, tokenFuture); @@ -475,7 +476,7 @@ public class TokenServiceTests extends ESTestCase { authentication = token.getAuthentication(); ThreadContext requestContext = new ThreadContext(Settings.EMPTY); - requestContext.putHeader("Authorization", "Bearer " + tokenService.getAccessTokenAsString(token)); + storeTokenHeader(requestContext, tokenService.getAccessTokenAsString(token)); try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { // the clock is still frozen, so the cookie should be valid @@ -519,10 +520,10 @@ public class TokenServiceTests extends ESTestCase { TokenService tokenService = new TokenService(Settings.builder() .put(XPackSettings.TOKEN_SERVICE_ENABLED_SETTING.getKey(), false) .build(), - Clock.systemUTC(), client, securityMainIndex, securityTokensIndex, clusterService); + Clock.systemUTC(), client, licenseState, securityMainIndex, securityTokensIndex, clusterService); IllegalStateException e = expectThrows(IllegalStateException.class, () -> tokenService.createOAuth2Tokens(null, null, null, true, null)); - assertEquals("tokens are not enabled", e.getMessage()); + assertEquals("security tokens are not enabled", e.getMessage()); PlainActionFuture future = new PlainActionFuture<>(); tokenService.getAndValidateToken(null, future); @@ -533,7 +534,7 @@ public class TokenServiceTests extends ESTestCase { tokenService.invalidateAccessToken((String) null, invalidateFuture); invalidateFuture.actionGet(); }); - assertEquals("tokens are not enabled", e.getMessage()); + assertEquals("security tokens are not enabled", e.getMessage()); } public void testBytesKeyEqualsHashCode() { @@ -562,11 +563,10 @@ public class TokenServiceTests extends ESTestCase { final int numBytes = randomIntBetween(1, TokenService.MINIMUM_BYTES + 32); final byte[] randomBytes = new byte[numBytes]; random().nextBytes(randomBytes); - TokenService tokenService = new TokenService(Settings.EMPTY, systemUTC(), client, securityMainIndex, securityTokensIndex, - clusterService); + TokenService tokenService = createTokenService(Settings.EMPTY, systemUTC()); ThreadContext requestContext = new ThreadContext(Settings.EMPTY); - requestContext.putHeader("Authorization", "Bearer " + Base64.getEncoder().encodeToString(randomBytes)); + storeTokenHeader(requestContext, Base64.getEncoder().encodeToString(randomBytes)); try (ThreadContext.StoredContext ignore = requestContext.newStoredContext(true)) { PlainActionFuture future = new PlainActionFuture<>(); @@ -576,8 +576,7 @@ public class TokenServiceTests extends ESTestCase { } public void testIndexNotAvailable() throws Exception { - TokenService tokenService = new TokenService(tokenServiceEnabledSettings, systemUTC(), client, securityMainIndex, - securityTokensIndex, clusterService); + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, systemUTC()); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); PlainActionFuture> tokenFuture = new PlainActionFuture<>(); tokenService.createOAuth2Tokens(authentication, authentication, Collections.emptyMap(), true, tokenFuture); @@ -586,7 +585,7 @@ public class TokenServiceTests extends ESTestCase { //mockGetTokenFromId(token, false); ThreadContext requestContext = new ThreadContext(Settings.EMPTY); - requestContext.putHeader("Authorization", "Bearer " + tokenService.getAccessTokenAsString(token)); + storeTokenHeader(requestContext, tokenService.getAccessTokenAsString(token)); doAnswer(invocationOnMock -> { ActionListener listener = (ActionListener) invocationOnMock.getArguments()[1]; @@ -630,8 +629,7 @@ public class TokenServiceTests extends ESTestCase { } public void testGetAuthenticationWorksWithExpiredUserToken() throws Exception { - TokenService tokenService = new TokenService(tokenServiceEnabledSettings, Clock.systemUTC(), client, securityMainIndex, - securityTokensIndex, clusterService); + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, Clock.systemUTC()); Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); UserToken expired = new UserToken(authentication, Instant.now().minus(3L, ChronoUnit.DAYS)); mockGetTokenFromId(expired, false); @@ -642,6 +640,27 @@ public class TokenServiceTests extends ESTestCase { assertEquals(authentication, retrievedAuth); } + public void testCannotValidateTokenIfLicenseDoesNotAllowTokens() throws Exception { + when(licenseState.isTokenServiceAllowed()).thenReturn(true); + TokenService tokenService = createTokenService(tokenServiceEnabledSettings, Clock.systemUTC()); + Authentication authentication = new Authentication(new User("joe", "admin"), new RealmRef("native_realm", "native", "node1"), null); + UserToken token = new UserToken(authentication, Instant.now().plusSeconds(180)); + mockGetTokenFromId(token, false); + + final ThreadContext threadContext = new ThreadContext(Settings.EMPTY); + storeTokenHeader(threadContext, tokenService.getAccessTokenAsString(token)); + + PlainActionFuture authFuture = new PlainActionFuture<>(); + when(licenseState.isTokenServiceAllowed()).thenReturn(false); + tokenService.getAndValidateToken(threadContext, authFuture); + UserToken authToken = authFuture.actionGet(); + assertThat(authToken, Matchers.nullValue()); + } + + private TokenService createTokenService(Settings settings, Clock clock) throws GeneralSecurityException { + return new TokenService(settings, clock, client, licenseState, securityMainIndex, securityTokensIndex, clusterService); + } + private void mockGetTokenFromId(UserToken userToken, boolean isExpired) { mockGetTokenFromId(userToken, isExpired, client); } @@ -700,23 +719,6 @@ public class TokenServiceTests extends ESTestCase { } } - private SecurityIndexManager mockSecurityManager() { - SecurityIndexManager mockSecurityIndex = mock(SecurityIndexManager.class); - doAnswer(invocationOnMock -> { - Runnable runnable = (Runnable) invocationOnMock.getArguments()[1]; - runnable.run(); - return null; - }).when(mockSecurityIndex).prepareIndexIfNeededThenExecute(any(Consumer.class), any(Runnable.class)); - doAnswer(invocationOnMock -> { - Runnable runnable = (Runnable) invocationOnMock.getArguments()[1]; - runnable.run(); - return null; - }).when(mockSecurityIndex).checkIndexVersionThenExecute(any(Consumer.class), any(Runnable.class)); - when(mockSecurityIndex.indexExists()).thenReturn(true); - when(mockSecurityIndex.isAvailable()).thenReturn(true); - return mockSecurityIndex; - } - private DiscoveryNode addAnotherDataNodeWithVersion(ClusterService clusterService, Version version) { final ClusterState currentState = clusterService.state(); final DiscoveryNodes.Builder discoBuilder = DiscoveryNodes.builder(currentState.getNodes()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandlerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandlerTests.java index 4ff582f01bd..4b40d165b5e 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandlerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandlerTests.java @@ -7,6 +7,7 @@ package org.elasticsearch.xpack.security.rest.action; import org.elasticsearch.client.node.NodeClient; import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.License; import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.RestRequest; import org.elasticsearch.test.ESTestCase; @@ -24,11 +25,13 @@ import static org.mockito.Mockito.when; public class SecurityBaseRestHandlerTests extends ESTestCase { public void testSecurityBaseRestHandlerChecksLicenseState() throws Exception { - final boolean securityDisabledByTrial = randomBoolean(); + final boolean securityDisabledByLicenseDefaults = randomBoolean(); final AtomicBoolean consumerCalled = new AtomicBoolean(false); final XPackLicenseState licenseState = mock(XPackLicenseState.class); when(licenseState.isSecurityAvailable()).thenReturn(true); - when(licenseState.isSecurityDisabledByTrialLicense()).thenReturn(securityDisabledByTrial); + when(licenseState.isSecurityDisabledByLicenseDefaults()).thenReturn(securityDisabledByLicenseDefaults); + when(licenseState.getOperationMode()).thenReturn( + randomFrom(License.OperationMode.BASIC, License.OperationMode.STANDARD, License.OperationMode.GOLD)); SecurityBaseRestHandler handler = new SecurityBaseRestHandler(Settings.EMPTY, licenseState) { @Override @@ -46,7 +49,7 @@ public class SecurityBaseRestHandlerTests extends ESTestCase { } }; FakeRestRequest fakeRestRequest = new FakeRestRequest(); - FakeRestChannel fakeRestChannel = new FakeRestChannel(fakeRestRequest, randomBoolean(), securityDisabledByTrial ? 1 : 0); + FakeRestChannel fakeRestChannel = new FakeRestChannel(fakeRestRequest, randomBoolean(), securityDisabledByLicenseDefaults ? 1 : 0); NodeClient client = mock(NodeClient.class); assertFalse(consumerCalled.get()); @@ -54,7 +57,7 @@ public class SecurityBaseRestHandlerTests extends ESTestCase { handler.handleRequest(fakeRestRequest, fakeRestChannel, client); verify(licenseState).isSecurityAvailable(); - if (securityDisabledByTrial == false) { + if (securityDisabledByLicenseDefaults == false) { assertTrue(consumerCalled.get()); assertEquals(0, fakeRestChannel.responses().get()); assertEquals(0, fakeRestChannel.errors().get()); diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestCreateApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyActionTests.java similarity index 95% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestCreateApiKeyActionTests.java rename to x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyActionTests.java index 1b1b0fe8f0f..394c9747b6d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestCreateApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestCreateApiKeyActionTests.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.security.rest.action; +package org.elasticsearch.xpack.security.rest.action.apikey; import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchSecurityException; @@ -30,6 +30,7 @@ import org.elasticsearch.test.rest.FakeRestRequest; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.security.action.CreateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.CreateApiKeyResponse; +import org.elasticsearch.xpack.security.rest.action.apikey.RestCreateApiKeyAction; import java.time.Duration; import java.time.Instant; @@ -56,6 +57,7 @@ public class RestCreateApiKeyActionTests extends ESTestCase { .build(); threadPool = new ThreadPool(settings); when(mockLicenseState.isSecurityAvailable()).thenReturn(true); + when(mockLicenseState.isApiKeyServiceAllowed()).thenReturn(true); } @Override diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestGetApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java similarity index 97% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestGetApiKeyActionTests.java rename to x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java index 533fa6195ed..9788bc1a5b2 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestGetApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestGetApiKeyActionTests.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.security.rest.action; +package org.elasticsearch.xpack.security.rest.action.apikey; import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchSecurityException; @@ -31,6 +31,7 @@ import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.security.action.ApiKey; import org.elasticsearch.xpack.core.security.action.GetApiKeyRequest; import org.elasticsearch.xpack.core.security.action.GetApiKeyResponse; +import org.elasticsearch.xpack.security.rest.action.apikey.RestGetApiKeyAction; import java.time.Instant; import java.time.temporal.ChronoUnit; @@ -56,6 +57,7 @@ public class RestGetApiKeyActionTests extends ESTestCase { .put(Environment.PATH_HOME_SETTING.getKey(), createTempDir().toString()).build(); threadPool = new ThreadPool(settings); when(mockLicenseState.isSecurityAvailable()).thenReturn(true); + when(mockLicenseState.isApiKeyServiceAllowed()).thenReturn(true); } @Override diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateApiKeyActionTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestInvalidateApiKeyActionTests.java similarity index 96% rename from x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateApiKeyActionTests.java rename to x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestInvalidateApiKeyActionTests.java index 6a8a60ae2a9..e73f4e3c210 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/RestInvalidateApiKeyActionTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/apikey/RestInvalidateApiKeyActionTests.java @@ -4,7 +4,7 @@ * you may not use this file except in compliance with the Elastic License. */ -package org.elasticsearch.xpack.security.rest.action; +package org.elasticsearch.xpack.security.rest.action.apikey; import org.apache.lucene.util.SetOnce; import org.elasticsearch.ElasticsearchSecurityException; @@ -29,6 +29,7 @@ import org.elasticsearch.test.rest.FakeRestRequest; import org.elasticsearch.threadpool.ThreadPool; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyRequest; import org.elasticsearch.xpack.core.security.action.InvalidateApiKeyResponse; +import org.elasticsearch.xpack.security.rest.action.apikey.RestInvalidateApiKeyAction; import java.util.Collections; @@ -52,6 +53,7 @@ public class RestInvalidateApiKeyActionTests extends ESTestCase { .build(); threadPool = new ThreadPool(settings); when(mockLicenseState.isSecurityAvailable()).thenReturn(true); + when(mockLicenseState.isApiKeyServiceAllowed()).thenReturn(true); } @Override diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/saml/SamlBaseRestHandlerTests.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/saml/SamlBaseRestHandlerTests.java index 5b442deacf6..66993c2269d 100644 --- a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/saml/SamlBaseRestHandlerTests.java +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/saml/SamlBaseRestHandlerTests.java @@ -27,16 +27,9 @@ public class SamlBaseRestHandlerTests extends ESTestCase { assertThat(handler.checkFeatureAvailable(new FakeRestRequest()), Matchers.nullValue()); } - public void testSecurityNotAvailableOnBasic() { - final SamlBaseRestHandler handler = buildHandler(License.OperationMode.BASIC); - Exception e = handler.checkFeatureAvailable(new FakeRestRequest()); - assertThat(e, instanceOf(ElasticsearchException.class)); - ElasticsearchException elasticsearchException = (ElasticsearchException) e; - assertThat(elasticsearchException.getMetadata(LicenseUtils.EXPIRED_FEATURE_METADATA), contains("security")); - } - - public void testSamlNotAvailableOnStandardOrGold() { - final SamlBaseRestHandler handler = buildHandler(randomFrom(License.OperationMode.STANDARD, License.OperationMode.GOLD)); + public void testSamlNotAvailableOnBasicStandardOrGold() { + final SamlBaseRestHandler handler = buildHandler(randomFrom(License.OperationMode.BASIC, License.OperationMode.STANDARD, + License.OperationMode.GOLD)); Exception e = handler.checkFeatureAvailable(new FakeRestRequest()); assertThat(e, instanceOf(ElasticsearchException.class)); ElasticsearchException elasticsearchException = (ElasticsearchException) e; diff --git a/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/test/SecurityMocks.java b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/test/SecurityMocks.java new file mode 100644 index 00000000000..5ff329ceced --- /dev/null +++ b/x-pack/plugin/security/src/test/java/org/elasticsearch/xpack/security/test/SecurityMocks.java @@ -0,0 +1,94 @@ +/* + * 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.xpack.security.test; + +import org.elasticsearch.action.ActionListener; +import org.elasticsearch.action.get.GetAction; +import org.elasticsearch.action.get.GetRequest; +import org.elasticsearch.action.get.GetRequestBuilder; +import org.elasticsearch.action.get.GetResponse; +import org.elasticsearch.client.Client; +import org.elasticsearch.common.bytes.BytesReference; +import org.elasticsearch.index.get.GetResult; +import org.elasticsearch.xpack.security.support.SecurityIndexManager; +import org.junit.Assert; + +import java.util.function.Consumer; + +import static java.util.Collections.emptyMap; +import static org.elasticsearch.index.mapper.MapperService.SINGLE_MAPPING_NAME; +import static org.elasticsearch.xpack.core.security.index.RestrictedIndicesNames.SECURITY_INDEX_NAME; +import static org.hamcrest.Matchers.arrayWithSize; +import static org.hamcrest.Matchers.equalTo; +import static org.hamcrest.Matchers.instanceOf; +import static org.mockito.Matchers.any; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Utility class for constructing commonly used mock objects. + * Note to maintainers: + * It is not intended that this class cover _all_ mocking scenarios. Consider very carefully before adding methods to this class that are + * only used in one or 2 places. This class is intended for the situations where a common piece of complex mock code is used in multiple + * test suites. + */ +public final class SecurityMocks { + + private SecurityMocks() { + throw new IllegalStateException("Cannot instantiate utility class"); + } + + public static SecurityIndexManager mockSecurityIndexManager() { + return mockSecurityIndexManager(true, true); + } + + public static SecurityIndexManager mockSecurityIndexManager(boolean exists, boolean available) { + final SecurityIndexManager securityIndexManager = mock(SecurityIndexManager.class); + doAnswer(invocationOnMock -> { + Runnable runnable = (Runnable) invocationOnMock.getArguments()[1]; + runnable.run(); + return null; + }).when(securityIndexManager).prepareIndexIfNeededThenExecute(any(Consumer.class), any(Runnable.class)); + doAnswer(invocationOnMock -> { + Runnable runnable = (Runnable) invocationOnMock.getArguments()[1]; + runnable.run(); + return null; + }).when(securityIndexManager).checkIndexVersionThenExecute(any(Consumer.class), any(Runnable.class)); + when(securityIndexManager.indexExists()).thenReturn(exists); + when(securityIndexManager.isAvailable()).thenReturn(available); + return securityIndexManager; + } + + public static void mockGetRequest(Client client, String documentId, BytesReference source) { + GetResult result = new GetResult(SECURITY_INDEX_NAME, SINGLE_MAPPING_NAME, documentId, 0, 1, 1, true, source, emptyMap()); + mockGetRequest(client, documentId, result); + } + + public static void mockGetRequest(Client client, String documentId, GetResult result) { + final GetRequestBuilder requestBuilder = new GetRequestBuilder(client, GetAction.INSTANCE); + requestBuilder.setIndex(SECURITY_INDEX_NAME); + requestBuilder.setType(SINGLE_MAPPING_NAME); + requestBuilder.setId(documentId); + when(client.prepareGet(SECURITY_INDEX_NAME, SINGLE_MAPPING_NAME, documentId)).thenReturn(requestBuilder); + + doAnswer(inv -> { + Assert.assertThat(inv.getArguments(), arrayWithSize(2)); + Assert.assertThat(inv.getArguments()[0], instanceOf(GetRequest.class)); + final GetRequest request = (GetRequest) inv.getArguments()[0]; + Assert.assertThat(request.id(), equalTo(documentId)); + Assert.assertThat(request.index(), equalTo(SECURITY_INDEX_NAME)); + Assert.assertThat(request.type(), equalTo(SINGLE_MAPPING_NAME)); + + Assert.assertThat(inv.getArguments()[1], instanceOf(ActionListener.class)); + ActionListener listener = (ActionListener) inv.getArguments()[1]; + listener.onResponse(new GetResponse(result)); + + return null; + }).when(client).get(any(GetRequest.class), any(ActionListener.class)); + } +}