From 063ed78c4227ea433af9eefae7f9e88b861e9422 Mon Sep 17 00:00:00 2001 From: Tim Vernum Date: Tue, 20 Mar 2018 18:54:51 +1000 Subject: [PATCH] Provide clearer errors if SAML is not licensed (elastic/x-pack-elasticsearch#4096) SAML is only available on a Platinum license. If you try and use SAML on a Gold license, then the error message is misleading - it gives the equivalent of "cannot find saml realm". This change adds a standard license error of "current license is non-compliant for [saml]" if SAML rest actions are used when SAML is not licensed. Original commit: elastic/x-pack-elasticsearch@7c0e26d58eed46a4441abfec7293326c9d1da469 --- .../xpack/security/authc/Realms.java | 2 +- .../rest/action/SecurityBaseRestHandler.java | 20 +++++-- .../saml/RestSamlAuthenticateAction.java | 2 +- .../saml/RestSamlInvalidateSessionAction.java | 2 +- .../action/saml/RestSamlLogoutAction.java | 3 +- .../RestSamlPrepareAuthenticationAction.java | 2 +- .../rest/action/saml/SamlBaseRestHandler.java | 38 ++++++++++++++ .../action/saml/SamlBaseRestHandlerTests.java | 52 +++++++++++++++++++ 8 files changed, 111 insertions(+), 10 deletions(-) create mode 100644 plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/SamlBaseRestHandler.java create mode 100644 plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/saml/SamlBaseRestHandlerTests.java diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/Realms.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/Realms.java index 83bbeebe839..8666526e4f8 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/Realms.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/authc/Realms.java @@ -266,7 +266,7 @@ public class Realms extends AbstractComponent implements Iterable { return converted; } - private static boolean isRealmTypeAvailable(AllowedRealmType enabledRealmType, String type) { + public static boolean isRealmTypeAvailable(AllowedRealmType enabledRealmType, String type) { switch (enabledRealmType) { case ALL: return true; diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandler.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandler.java index 949645f08cf..ffa5158ab6b 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandler.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/SecurityBaseRestHandler.java @@ -12,11 +12,10 @@ import org.elasticsearch.license.XPackLicenseState; import org.elasticsearch.rest.BaseRestHandler; import org.elasticsearch.rest.BytesRestResponse; import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.xpack.core.XPackField; import java.io.IOException; -import static org.elasticsearch.xpack.core.XPackField.SECURITY; - /** * Base class for security rest handlers. This handler takes care of ensuring that the license * level is valid so that security can be used! @@ -45,13 +44,26 @@ public abstract class SecurityBaseRestHandler extends BaseRestHandler { */ protected final RestChannelConsumer prepareRequest(RestRequest request, NodeClient client) throws IOException { RestChannelConsumer consumer = innerPrepareRequest(request, client); - if (licenseState.isAuthAllowed()) { + final String failedFeature = checkLicensedFeature(request); + if (failedFeature == null) { return consumer; } else { - return channel -> channel.sendResponse(new BytesRestResponse(channel, LicenseUtils.newComplianceException(SECURITY))); + return channel -> channel.sendResponse(new BytesRestResponse(channel, LicenseUtils.newComplianceException(failedFeature))); } } + /** + * Check whether the given request is allowed within the current license state, and return the name of any unlicensed feature. + * By default this returns {@link org.elasticsearch.xpack.core.XPackField#SECURITY} if the license state does not + * {@link XPackLicenseState#isAuthAllowed() allow authentication and authorization}. + * Sub-classes can override this method if they have additional licensing requirements. + * @return {@code null} if all required features are licensed, otherwise the name of the most significant unlicensed feature. + */ + protected String checkLicensedFeature(RestRequest request) { + return licenseState.isAuthAllowed() ? null : XPackField.SECURITY; + } + + /** * Implementers should implement this method as they normally would for * {@link BaseRestHandler#prepareRequest(RestRequest, NodeClient)} and ensure that all request diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlAuthenticateAction.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlAuthenticateAction.java index 7649f9cba1d..8f241b0b14f 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlAuthenticateAction.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlAuthenticateAction.java @@ -34,7 +34,7 @@ import static org.elasticsearch.rest.RestRequest.Method.POST; /** * A REST handler that attempts to authenticate a user based on the provided SAML response/assertion. */ -public class RestSamlAuthenticateAction extends SecurityBaseRestHandler implements RestHandler { +public class RestSamlAuthenticateAction extends SamlBaseRestHandler implements RestHandler { static class Input { String content; diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlInvalidateSessionAction.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlInvalidateSessionAction.java index 2ca45327a84..ac287a13d16 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlInvalidateSessionAction.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlInvalidateSessionAction.java @@ -31,7 +31,7 @@ import static org.elasticsearch.rest.RestRequest.Method.POST; * Invalidates any security tokens associated with the provided SAML session. * The session identity is provided in a SAML {@code <LogoutRequest>} */ -public class RestSamlInvalidateSessionAction extends SecurityBaseRestHandler { +public class RestSamlInvalidateSessionAction extends SamlBaseRestHandler { static final ObjectParser PARSER = new ObjectParser<>("saml_invalidate_session", SamlInvalidateSessionRequest::new); diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlLogoutAction.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlLogoutAction.java index 7a17b9202f2..bc81f2d6bab 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlLogoutAction.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlLogoutAction.java @@ -23,7 +23,6 @@ import org.elasticsearch.rest.action.RestBuilderListener; import org.elasticsearch.xpack.core.security.action.saml.SamlLogoutAction; import org.elasticsearch.xpack.core.security.action.saml.SamlLogoutRequest; import org.elasticsearch.xpack.core.security.action.saml.SamlLogoutResponse; -import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; import static org.elasticsearch.rest.RestRequest.Method.POST; @@ -33,7 +32,7 @@ import static org.elasticsearch.rest.RestRequest.Method.POST; * This logout request is returned in the REST response as a redirect URI, and the REST client should * make it available to the browser. */ -public class RestSamlLogoutAction extends SecurityBaseRestHandler { +public class RestSamlLogoutAction extends SamlBaseRestHandler { static final ObjectParser PARSER = new ObjectParser<>("saml_logout", SamlLogoutRequest::new); diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlPrepareAuthenticationAction.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlPrepareAuthenticationAction.java index 08bd864fc30..e5b6cdc4942 100644 --- a/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlPrepareAuthenticationAction.java +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/RestSamlPrepareAuthenticationAction.java @@ -33,7 +33,7 @@ import static org.elasticsearch.rest.RestRequest.Method.POST; * The request is returned in the REST response, and the REST client should make it available * to the browser. */ -public class RestSamlPrepareAuthenticationAction extends SecurityBaseRestHandler { +public class RestSamlPrepareAuthenticationAction extends SamlBaseRestHandler { static final ObjectParser PARSER = new ObjectParser<>("saml_prepare_authn", SamlPrepareAuthenticationRequest::new); diff --git a/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/SamlBaseRestHandler.java b/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/SamlBaseRestHandler.java new file mode 100644 index 00000000000..46f4bb35121 --- /dev/null +++ b/plugin/security/src/main/java/org/elasticsearch/xpack/security/rest/action/saml/SamlBaseRestHandler.java @@ -0,0 +1,38 @@ +/* + * 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.saml; + +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.XPackLicenseState; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.xpack.core.security.authc.saml.SamlRealmSettings; +import org.elasticsearch.xpack.security.authc.Realms; +import org.elasticsearch.xpack.security.rest.action.SecurityBaseRestHandler; + +/** + * An abstract implementation of {@link SecurityBaseRestHandler} that performs a license check for the SAML realm type + */ +public abstract class SamlBaseRestHandler extends SecurityBaseRestHandler { + + private static final String SAML_REALM_TYPE = SamlRealmSettings.TYPE; + + public SamlBaseRestHandler(Settings settings, XPackLicenseState licenseState) { + super(settings, licenseState); + } + + @Override + protected String checkLicensedFeature(RestRequest request) { + String feature = super.checkLicensedFeature(request); + if (feature != null) { + return feature; + } else if (Realms.isRealmTypeAvailable(licenseState.allowedRealmType(), SAML_REALM_TYPE)) { + return null; + } else { + logger.info("The '{}' realm is not available under the current license", SAML_REALM_TYPE); + return SAML_REALM_TYPE; + } + } +} diff --git a/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/saml/SamlBaseRestHandlerTests.java b/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/saml/SamlBaseRestHandlerTests.java new file mode 100644 index 00000000000..7bc6fccb811 --- /dev/null +++ b/plugin/security/src/test/java/org/elasticsearch/xpack/security/rest/action/saml/SamlBaseRestHandlerTests.java @@ -0,0 +1,52 @@ +/* + * 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.saml; + +import org.elasticsearch.client.node.NodeClient; +import org.elasticsearch.common.settings.Settings; +import org.elasticsearch.license.License; +import org.elasticsearch.license.TestUtils; +import org.elasticsearch.rest.RestRequest; +import org.elasticsearch.test.ESTestCase; +import org.elasticsearch.test.rest.FakeRestRequest; +import org.hamcrest.Matchers; + +public class SamlBaseRestHandlerTests extends ESTestCase { + + public void testSamlAvailableOnTrialAndPlatinum() { + final SamlBaseRestHandler handler = buildHandler(randomFrom(License.OperationMode.TRIAL, License.OperationMode.PLATINUM)); + assertThat(handler.checkLicensedFeature(new FakeRestRequest()), Matchers.nullValue()); + } + + public void testSecurityNotAvailableOnBasic() { + final SamlBaseRestHandler handler = buildHandler(License.OperationMode.BASIC); + assertThat(handler.checkLicensedFeature(new FakeRestRequest()), Matchers.equalTo("security")); + } + + public void testSamlNotAvailableOnStandardOrGold() { + final SamlBaseRestHandler handler = buildHandler(randomFrom(License.OperationMode.STANDARD, License.OperationMode.GOLD)); + assertThat(handler.checkLicensedFeature(new FakeRestRequest()), Matchers.equalTo("saml")); + } + + private SamlBaseRestHandler buildHandler(License.OperationMode licenseMode) { + final TestUtils.UpdatableLicenseState licenseState = new TestUtils.UpdatableLicenseState(); + licenseState.update(licenseMode, true); + + return new SamlBaseRestHandler(Settings.EMPTY, licenseState) { + + @Override + public String getName() { + return "saml_test"; + } + + @Override + protected RestChannelConsumer innerPrepareRequest(RestRequest request, NodeClient client) { + return null; + } + }; + } + +} \ No newline at end of file