From e50858c314a138e2c2ced50bee9a5c2754929f8b Mon Sep 17 00:00:00 2001 From: Gregory Chanan Date: Mon, 25 Jul 2016 14:15:48 -0400 Subject: [PATCH] SOLR-9324: Support Secure Impersonation / Proxy User for solr authentication --- solr/CHANGES.txt | 3 + .../DelegationTokenKerberosFilter.java | 46 ++- .../apache/solr/security/KerberosPlugin.java | 223 ++++++------ .../TestSolrCloudWithDelegationTokens.java | 9 +- .../TestSolrCloudWithSecureImpersonation.java | 340 ++++++++++++++++++ ...mDelegationTokenAuthenticationHandler.java | 109 ------ .../HttpParamDelegationTokenPlugin.java | 272 ++++++++++++++ 7 files changed, 785 insertions(+), 217 deletions(-) create mode 100644 solr/core/src/test/org/apache/solr/cloud/TestSolrCloudWithSecureImpersonation.java delete mode 100644 solr/core/src/test/org/apache/solr/security/HttpParamDelegationTokenAuthenticationHandler.java create mode 100644 solr/core/src/test/org/apache/solr/security/HttpParamDelegationTokenPlugin.java diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index c2a69fdab82..538edce3c75 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -111,6 +111,9 @@ New Features * SOLR-9279: New boolean comparison function queries comparing numeric arguments: gt, gte, lt, lte, eq (Doug Turnbull, David Smiley) +* SOLR-9346: Support Secure Impersonation / Proxy User for solr authentication + (Gregory Chanan) + Bug Fixes ---------------------- diff --git a/solr/core/src/java/org/apache/solr/security/DelegationTokenKerberosFilter.java b/solr/core/src/java/org/apache/solr/security/DelegationTokenKerberosFilter.java index 7dbb1ad8545..ca27861e8c4 100644 --- a/solr/core/src/java/org/apache/solr/security/DelegationTokenKerberosFilter.java +++ b/solr/core/src/java/org/apache/solr/security/DelegationTokenKerberosFilter.java @@ -18,6 +18,7 @@ package org.apache.solr.security; import java.io.IOException; import java.lang.invoke.MethodHandles; +import java.util.Enumeration; import java.util.LinkedList; import java.util.List; @@ -36,8 +37,11 @@ import org.apache.curator.framework.CuratorFrameworkFactory; import org.apache.curator.framework.api.ACLProvider; import org.apache.curator.retry.ExponentialBackoffRetry; +import org.apache.hadoop.conf.Configuration; +import org.apache.hadoop.security.UserGroupInformation; import org.apache.hadoop.security.authentication.server.AuthenticationHandler; import org.apache.hadoop.security.token.delegation.web.DelegationTokenAuthenticationFilter; +import org.apache.hadoop.security.token.delegation.web.HttpUserGroupInformation; import org.apache.solr.common.cloud.SecurityAwareZkACLProvider; import org.apache.solr.common.cloud.SolrZkClient; import org.apache.solr.common.cloud.ZkACLProvider; @@ -62,6 +66,27 @@ public class DelegationTokenKerberosFilter extends DelegationTokenAuthentication super.init(conf); } + /** + * Return the ProxyUser Configuration. FilterConfig properties beginning with + * "solr.impersonator.user.name" will be added to the configuration. + */ + @Override + protected Configuration getProxyuserConfiguration(FilterConfig filterConf) + throws ServletException { + Configuration conf = new Configuration(false); + + Enumeration names = filterConf.getInitParameterNames(); + while (names.hasMoreElements()) { + String name = (String) names.nextElement(); + if (name.startsWith(KerberosPlugin.IMPERSONATOR_PREFIX)) { + String value = filterConf.getInitParameter(name); + conf.set(PROXYUSER_PREFIX + "." + name.substring(KerberosPlugin.IMPERSONATOR_PREFIX.length()), value); + conf.set(name, value); + } + } + return conf; + } + @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { @@ -76,7 +101,26 @@ public class DelegationTokenKerberosFilter extends DelegationTokenAuthentication return nonNullQueryString; } }; - super.doFilter(requestNonNullQueryString, response, filterChain); + + // include Impersonator User Name in case someone (e.g. logger) wants it + FilterChain filterChainWrapper = new FilterChain() { + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse) + throws IOException, ServletException { + HttpServletRequest httpRequest = (HttpServletRequest) servletRequest; + + UserGroupInformation ugi = HttpUserGroupInformation.get(); + if (ugi != null && ugi.getAuthenticationMethod() == UserGroupInformation.AuthenticationMethod.PROXY) { + UserGroupInformation realUserUgi = ugi.getRealUser(); + if (realUserUgi != null) { + httpRequest.setAttribute(KerberosPlugin.IMPERSONATOR_USER_NAME, realUserUgi.getShortUserName()); + } + } + filterChain.doFilter(servletRequest, servletResponse); + } + }; + + super.doFilter(requestNonNullQueryString, response, filterChainWrapper); } @Override diff --git a/solr/core/src/java/org/apache/solr/security/KerberosPlugin.java b/solr/core/src/java/org/apache/solr/security/KerberosPlugin.java index 1cd476fc8f8..d4a282359ef 100644 --- a/solr/core/src/java/org/apache/solr/security/KerberosPlugin.java +++ b/solr/core/src/java/org/apache/solr/security/KerberosPlugin.java @@ -69,7 +69,7 @@ public class KerberosPlugin extends AuthenticationPlugin implements HttpClientBu private static final Logger log = LoggerFactory.getLogger(MethodHandles.lookup().lookupClass()); Krb5HttpClientBuilder kerberosBuilder = new Krb5HttpClientBuilder(); - Filter kerberosFilter; + private Filter kerberosFilter; public static final String NAME_RULES_PARAM = "solr.kerberos.name.rules"; public static final String COOKIE_DOMAIN_PARAM = "solr.kerberos.cookie.domain"; @@ -78,6 +78,7 @@ public class KerberosPlugin extends AuthenticationPlugin implements HttpClientBu public static final String KEYTAB_PARAM = "solr.kerberos.keytab"; public static final String TOKEN_VALID_PARAM = "solr.kerberos.token.valid"; public static final String COOKIE_PORT_AWARE_PARAM = "solr.kerberos.cookie.portaware"; + public static final String IMPERSONATOR_PREFIX = "solr.kerberos.impersonator.user."; public static final String DELEGATION_TOKEN_ENABLED = "solr.kerberos.delegation.token.enabled"; public static final String DELEGATION_TOKEN_KIND = "solr.kerberos.delegation.token.kind"; public static final String DELEGATION_TOKEN_VALIDITY = "solr.kerberos.delegation.token.validity"; @@ -86,18 +87,17 @@ public class KerberosPlugin extends AuthenticationPlugin implements HttpClientBu "solr.kerberos.delegation.token.signer.secret.provider.zookeper.path"; public static final String DELEGATION_TOKEN_SECRET_MANAGER_ZNODE_WORKING_PATH = "solr.kerberos.delegation.token.secret.manager.znode.working.path"; + public static final String DELEGATION_TOKEN_TYPE_DEFAULT = "solr-dt"; - + public static final String IMPERSONATOR_DO_AS_HTTP_PARAM = "doAs"; + public static final String IMPERSONATOR_USER_NAME = "solr.impersonator.user.name"; + // filled in by Plugin/Filter static final String REQUEST_CONTINUES_ATTR = "org.apache.solr.security.kerberosplugin.requestcontinues"; static final String DELEGATION_TOKEN_ZK_CLIENT = "solr.kerberos.delegation.token.zk.client"; - // allows test to specify an alternate auth handler - @VisibleForTesting - public static final String AUTH_HANDLER_PARAM = "solr.kerberos.auth.handler"; - private final CoreContainer coreContainer; public KerberosPlugin(CoreContainer coreContainer) { @@ -107,107 +107,123 @@ public class KerberosPlugin extends AuthenticationPlugin implements HttpClientBu @Override public void init(Map pluginConfig) { try { - Map params = new HashMap(); - putParam(params, "type", AUTH_HANDLER_PARAM, "kerberos"); - putParam(params, "kerberos.name.rules", NAME_RULES_PARAM, "DEFAULT"); - putParam(params, "token.valid", TOKEN_VALID_PARAM, "30"); - putParam(params, "cookie.path", COOKIE_PATH_PARAM, "/"); - if ("kerberos".equals(params.get("type"))) { - putParam(params, "kerberos.principal", PRINCIPAL_PARAM, null); - putParam(params, "kerberos.keytab", KEYTAB_PARAM, null); - } else { - // allow tests which specify AUTH_HANDLER_PARAM to avoid specifying kerberos principal/keytab - putParamOptional(params, "kerberos.principal", PRINCIPAL_PARAM); - putParamOptional(params, "kerberos.keytab", KEYTAB_PARAM); - } - - String delegationTokenStr = System.getProperty(DELEGATION_TOKEN_ENABLED, null); - boolean delegationTokenEnabled = - (delegationTokenStr == null) ? false : Boolean.parseBoolean(delegationTokenStr); - ZkController controller = coreContainer.getZkController(); - - if (delegationTokenEnabled) { - putParam(params, "delegation-token.token-kind", DELEGATION_TOKEN_KIND, DELEGATION_TOKEN_TYPE_DEFAULT); - if (coreContainer.isZooKeeperAware()) { - putParam(params, "signer.secret.provider", DELEGATION_TOKEN_SECRET_PROVIDER, "zookeeper"); - if ("zookeeper".equals(params.get("signer.secret.provider"))) { - String zkHost = controller.getZkServerAddress(); - putParam(params, "token.validity", DELEGATION_TOKEN_VALIDITY, "36000"); - params.put("zk-dt-secret-manager.enable", "true"); - // Note - Curator complains if the znodeWorkingPath starts with / - String chrootPath = zkHost.substring(zkHost.indexOf("/")); - String relativePath = chrootPath.startsWith("/") ? chrootPath.substring(1) : chrootPath; - putParam(params, "zk-dt-secret-manager.znodeWorkingPath", - DELEGATION_TOKEN_SECRET_MANAGER_ZNODE_WORKING_PATH, - relativePath + SecurityAwareZkACLProvider.SECURITY_ZNODE_PATH + "/zkdtsm"); - putParam(params, "signer.secret.provider.zookeeper.path", - DELEGATION_TOKEN_SECRET_PROVIDER_ZK_PATH, "/token"); - // ensure krb5 is setup properly before running curator - getHttpClientBuilder(SolrHttpClientBuilder.create()); - } - } else { - log.info("CoreContainer is not ZooKeeperAware, not setting ZK-related delegation token properties"); - } - } - - // Special handling for the "cookie.domain" based on whether port should be - // appended to the domain. Useful for situations where multiple solr nodes are - // on the same host. - String usePortStr = System.getProperty(COOKIE_PORT_AWARE_PARAM, null); - boolean needPortAwareCookies = (usePortStr == null) ? false: Boolean.parseBoolean(usePortStr); - - if (!needPortAwareCookies || !coreContainer.isZooKeeperAware()) { - putParam(params, "cookie.domain", COOKIE_DOMAIN_PARAM, null); - } else { // we need port aware cookies and we are in SolrCloud mode. - String host = System.getProperty(COOKIE_DOMAIN_PARAM, null); - if (host==null) { - throw new SolrException(ErrorCode.SERVER_ERROR, "Missing required parameter '"+COOKIE_DOMAIN_PARAM+"'."); - } - int port = controller.getHostPort(); - params.put("cookie.domain", host + ":" + port); - } - - final ServletContext servletContext = new AttributeOnlyServletContext(); - if (delegationTokenEnabled) { - kerberosFilter = new DelegationTokenKerberosFilter(); - // pass an attribute-enabled context in order to pass the zkClient - // and because the filter may pass a curator instance. - if (controller != null) { - servletContext.setAttribute(DELEGATION_TOKEN_ZK_CLIENT, controller.getZkClient()); - } - } else { - kerberosFilter = new KerberosFilter(); - } - log.info("Params: "+params); - - FilterConfig conf = new FilterConfig() { - @Override - public ServletContext getServletContext() { - return servletContext; - } - - @Override - public Enumeration getInitParameterNames() { - return new IteratorEnumeration(params.keySet().iterator()); - } - - @Override - public String getInitParameter(String param) { - return params.get(param); - } - - @Override - public String getFilterName() { - return "KerberosFilter"; - } - }; - + FilterConfig conf = getInitFilterConfig(pluginConfig, false); kerberosFilter.init(conf); } catch (ServletException e) { throw new SolrException(ErrorCode.SERVER_ERROR, "Error initializing kerberos authentication plugin: "+e); } } + @VisibleForTesting + protected FilterConfig getInitFilterConfig(Map pluginConfig, boolean skipKerberosChecking) { + Map params = new HashMap(); + params.put("type", "kerberos"); + putParam(params, "kerberos.name.rules", NAME_RULES_PARAM, "DEFAULT"); + putParam(params, "token.valid", TOKEN_VALID_PARAM, "30"); + putParam(params, "cookie.path", COOKIE_PATH_PARAM, "/"); + if (!skipKerberosChecking) { + putParam(params, "kerberos.principal", PRINCIPAL_PARAM, null); + putParam(params, "kerberos.keytab", KEYTAB_PARAM, null); + } else { + putParamOptional(params, "kerberos.principal", PRINCIPAL_PARAM); + putParamOptional(params, "kerberos.keytab", KEYTAB_PARAM); + } + + String delegationTokenStr = System.getProperty(DELEGATION_TOKEN_ENABLED, null); + boolean delegationTokenEnabled = + (delegationTokenStr == null) ? false : Boolean.parseBoolean(delegationTokenStr); + ZkController controller = coreContainer.getZkController(); + + if (delegationTokenEnabled) { + putParam(params, "delegation-token.token-kind", DELEGATION_TOKEN_KIND, DELEGATION_TOKEN_TYPE_DEFAULT); + if (coreContainer.isZooKeeperAware()) { + putParam(params, "signer.secret.provider", DELEGATION_TOKEN_SECRET_PROVIDER, "zookeeper"); + if ("zookeeper".equals(params.get("signer.secret.provider"))) { + String zkHost = controller.getZkServerAddress(); + putParam(params, "token.validity", DELEGATION_TOKEN_VALIDITY, "36000"); + params.put("zk-dt-secret-manager.enable", "true"); + // Note - Curator complains if the znodeWorkingPath starts with / + String chrootPath = zkHost.substring(zkHost.indexOf("/")); + String relativePath = chrootPath.startsWith("/") ? chrootPath.substring(1) : chrootPath; + putParam(params, "zk-dt-secret-manager.znodeWorkingPath", + DELEGATION_TOKEN_SECRET_MANAGER_ZNODE_WORKING_PATH, + relativePath + SecurityAwareZkACLProvider.SECURITY_ZNODE_PATH + "/zkdtsm"); + putParam(params, "signer.secret.provider.zookeeper.path", + DELEGATION_TOKEN_SECRET_PROVIDER_ZK_PATH, "/token"); + // ensure krb5 is setup properly before running curator + getHttpClientBuilder(SolrHttpClientBuilder.create()); + } + } else { + log.info("CoreContainer is not ZooKeeperAware, not setting ZK-related delegation token properties"); + } + } + + // Special handling for the "cookie.domain" based on whether port should be + // appended to the domain. Useful for situations where multiple solr nodes are + // on the same host. + String usePortStr = System.getProperty(COOKIE_PORT_AWARE_PARAM, null); + boolean needPortAwareCookies = (usePortStr == null) ? false: Boolean.parseBoolean(usePortStr); + + if (!needPortAwareCookies || !coreContainer.isZooKeeperAware()) { + putParam(params, "cookie.domain", COOKIE_DOMAIN_PARAM, null); + } else { // we need port aware cookies and we are in SolrCloud mode. + String host = System.getProperty(COOKIE_DOMAIN_PARAM, null); + if (host==null) { + throw new SolrException(ErrorCode.SERVER_ERROR, "Missing required parameter '"+COOKIE_DOMAIN_PARAM+"'."); + } + int port = controller.getHostPort(); + params.put("cookie.domain", host + ":" + port); + } + + // check impersonator config + for (Enumeration e = System.getProperties().propertyNames(); e.hasMoreElements();) { + String key = e.nextElement().toString(); + if (key.startsWith(IMPERSONATOR_PREFIX)) { + if (!delegationTokenEnabled) { + throw new SolrException(ErrorCode.SERVER_ERROR, + "Impersonator configuration requires delegation tokens to be enabled: " + key); + } + params.put(key, System.getProperty(key)); + } + } + final ServletContext servletContext = new AttributeOnlyServletContext(); + if (controller != null) { + servletContext.setAttribute(DELEGATION_TOKEN_ZK_CLIENT, controller.getZkClient()); + } + if (delegationTokenEnabled) { + kerberosFilter = new DelegationTokenKerberosFilter(); + // pass an attribute-enabled context in order to pass the zkClient + // and because the filter may pass a curator instance. + } else { + kerberosFilter = new KerberosFilter(); + } + log.info("Params: "+params); + + FilterConfig conf = new FilterConfig() { + @Override + public ServletContext getServletContext() { + return servletContext; + } + + @Override + public Enumeration getInitParameterNames() { + return new IteratorEnumeration(params.keySet().iterator()); + } + + @Override + public String getInitParameter(String param) { + return params.get(param); + } + + @Override + public String getFilterName() { + return "KerberosFilter"; + } + }; + + return conf; + } + private void putParam(Map params, String internalParamName, String externalParamName, String defaultValue) { String value = System.getProperty(externalParamName, defaultValue); if (value==null) { @@ -260,11 +276,16 @@ public class KerberosPlugin extends AuthenticationPlugin implements HttpClientBu return kerberosBuilder.getBuilder(builder); } + @Override public void close() { kerberosFilter.destroy(); kerberosBuilder.close(); } + protected Filter getKerberosFilter() { return kerberosFilter; } + + protected void setKerberosFilter(Filter kerberosFilter) { this.kerberosFilter = kerberosFilter; } + protected static class AttributeOnlyServletContext implements ServletContext { private Map attributes = new HashMap(); diff --git a/solr/core/src/test/org/apache/solr/cloud/TestSolrCloudWithDelegationTokens.java b/solr/core/src/test/org/apache/solr/cloud/TestSolrCloudWithDelegationTokens.java index ae1c4393b88..a58ec8c45b2 100644 --- a/solr/core/src/test/org/apache/solr/cloud/TestSolrCloudWithDelegationTokens.java +++ b/solr/core/src/test/org/apache/solr/cloud/TestSolrCloudWithDelegationTokens.java @@ -30,10 +30,10 @@ import org.apache.solr.common.SolrException.ErrorCode; import org.apache.solr.common.cloud.SolrZkClient; import org.apache.solr.common.params.SolrParams; import org.apache.solr.common.params.ModifiableSolrParams; -import static org.apache.solr.security.HttpParamDelegationTokenAuthenticationHandler.USER_PARAM; +import static org.apache.solr.security.HttpParamDelegationTokenPlugin.USER_PARAM; import org.apache.http.HttpStatus; -import org.apache.solr.security.HttpParamDelegationTokenAuthenticationHandler; +import org.apache.solr.security.HttpParamDelegationTokenPlugin; import org.apache.solr.security.KerberosPlugin; import org.junit.AfterClass; import org.junit.BeforeClass; @@ -59,10 +59,8 @@ public class TestSolrCloudWithDelegationTokens extends SolrTestCaseJ4 { @BeforeClass public static void startup() throws Exception { - System.setProperty("authenticationPlugin", KerberosPlugin.class.getName()); + System.setProperty("authenticationPlugin", HttpParamDelegationTokenPlugin.class.getName()); System.setProperty(KerberosPlugin.DELEGATION_TOKEN_ENABLED, "true"); - System.setProperty(KerberosPlugin.AUTH_HANDLER_PARAM, - HttpParamDelegationTokenAuthenticationHandler.class.getName()); System.setProperty("solr.kerberos.cookie.domain", "127.0.0.1"); miniCluster = new MiniSolrCloudCluster(NUM_SERVERS, createTempDir(), buildJettyConfig("/solr")); @@ -88,7 +86,6 @@ public class TestSolrCloudWithDelegationTokens extends SolrTestCaseJ4 { solrClientSecondary = null; System.clearProperty("authenticationPlugin"); System.clearProperty(KerberosPlugin.DELEGATION_TOKEN_ENABLED); - System.clearProperty(KerberosPlugin.AUTH_HANDLER_PARAM); System.clearProperty("solr.kerberos.cookie.domain"); } diff --git a/solr/core/src/test/org/apache/solr/cloud/TestSolrCloudWithSecureImpersonation.java b/solr/core/src/test/org/apache/solr/cloud/TestSolrCloudWithSecureImpersonation.java new file mode 100644 index 00000000000..1839d32760e --- /dev/null +++ b/solr/core/src/test/org/apache/solr/cloud/TestSolrCloudWithSecureImpersonation.java @@ -0,0 +1,340 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.cloud; + +import javax.servlet.http.HttpServletRequest; +import java.io.File; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.concurrent.atomic.AtomicBoolean; + +import org.apache.hadoop.conf.Configuration; +import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.client.solrj.SolrClient; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.embedded.JettySolrRunner; +import org.apache.solr.client.solrj.impl.HttpSolrClient; +import org.apache.solr.client.solrj.request.CollectionAdminRequest; +import org.apache.solr.client.solrj.response.CollectionAdminResponse; +import org.apache.solr.common.cloud.ZkStateReader; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.core.CoreContainer; +import org.apache.solr.handler.admin.CollectionsHandler; +import org.apache.solr.request.SolrQueryRequest; +import org.apache.solr.response.SolrQueryResponse; +import org.apache.solr.security.HttpParamDelegationTokenPlugin; +import org.apache.solr.security.KerberosPlugin; +import org.apache.solr.servlet.SolrRequestParsers; +import org.junit.AfterClass; +import org.junit.BeforeClass; +import org.junit.Test; + +import static org.apache.solr.security.HttpParamDelegationTokenPlugin.USER_PARAM; +import static org.apache.solr.security.HttpParamDelegationTokenPlugin.REMOTE_HOST_PARAM; +import static org.apache.solr.security.HttpParamDelegationTokenPlugin.REMOTE_ADDRESS_PARAM; + +public class TestSolrCloudWithSecureImpersonation extends SolrTestCaseJ4 { + private static final int NUM_SERVERS = 2; + private static MiniSolrCloudCluster miniCluster; + private static SolrClient solrClient; + + private static String getUsersFirstGroup() throws Exception { + org.apache.hadoop.security.Groups hGroups = + new org.apache.hadoop.security.Groups(new Configuration()); + List g = hGroups.getGroups(System.getProperty("user.name")); + return g.get(0); + } + + private static Map getImpersonatorSettings() throws Exception { + Map filterProps = new TreeMap(); + filterProps.put(KerberosPlugin.IMPERSONATOR_PREFIX + "noGroups.hosts", "*"); + filterProps.put(KerberosPlugin.IMPERSONATOR_PREFIX + "anyHostAnyUser.groups", "*"); + filterProps.put(KerberosPlugin.IMPERSONATOR_PREFIX + "anyHostAnyUser.hosts", "*"); + filterProps.put(KerberosPlugin.IMPERSONATOR_PREFIX + "wrongHost.hosts", "1.1.1.1.1.1"); + filterProps.put(KerberosPlugin.IMPERSONATOR_PREFIX + "wrongHost.groups", "*"); + filterProps.put(KerberosPlugin.IMPERSONATOR_PREFIX + "noHosts.groups", "*"); + filterProps.put(KerberosPlugin.IMPERSONATOR_PREFIX + "localHostAnyGroup.groups", "*"); + filterProps.put(KerberosPlugin.IMPERSONATOR_PREFIX + "localHostAnyGroup.hosts", "127.0.0.1"); + filterProps.put(KerberosPlugin.IMPERSONATOR_PREFIX + "anyHostUsersGroup.groups", getUsersFirstGroup()); + filterProps.put(KerberosPlugin.IMPERSONATOR_PREFIX + "anyHostUsersGroup.hosts", "*"); + filterProps.put(KerberosPlugin.IMPERSONATOR_PREFIX + "bogusGroup.groups", "__some_bogus_group"); + filterProps.put(KerberosPlugin.IMPERSONATOR_PREFIX + "bogusGroup.hosts", "*"); + return filterProps; + } + + @BeforeClass + public static void startup() throws Exception { + System.setProperty("authenticationPlugin", HttpParamDelegationTokenPlugin.class.getName()); + System.setProperty(KerberosPlugin.DELEGATION_TOKEN_ENABLED, "true"); + + System.setProperty("solr.kerberos.cookie.domain", "127.0.0.1"); + Map impSettings = getImpersonatorSettings(); + for (Map.Entry entry : impSettings.entrySet()) { + System.setProperty(entry.getKey(), entry.getValue()); + } + System.setProperty("solr.test.sys.prop1", "propone"); + System.setProperty("solr.test.sys.prop2", "proptwo"); + + SolrRequestParsers.DEFAULT.setAddRequestHeadersToContext(true); + String solrXml = MiniSolrCloudCluster.DEFAULT_CLOUD_SOLR_XML.replace("", + " " + ImpersonatorCollectionsHandler.class.getName() + "\n" + + ""); + + miniCluster = new MiniSolrCloudCluster(NUM_SERVERS, createTempDir(), solrXml, buildJettyConfig("/solr")); + JettySolrRunner runner = miniCluster.getJettySolrRunners().get(0); + solrClient = new HttpSolrClient.Builder(runner.getBaseUrl().toString()).build(); + } + + /** + * Verify that impersonator info is preserved in the request + */ + public static class ImpersonatorCollectionsHandler extends CollectionsHandler { + public static AtomicBoolean called = new AtomicBoolean(false); + + public ImpersonatorCollectionsHandler() { super(); } + + public ImpersonatorCollectionsHandler(final CoreContainer coreContainer) { + super(coreContainer); + } + + @Override + public void handleRequestBody(SolrQueryRequest req, SolrQueryResponse rsp) throws Exception { + called.set(true); + super.handleRequestBody(req, rsp); + String doAs = req.getParams().get(KerberosPlugin.IMPERSONATOR_DO_AS_HTTP_PARAM); + if (doAs != null) { + HttpServletRequest httpRequest = (HttpServletRequest)req.getContext().get("httpRequest"); + assertNotNull(httpRequest); + String user = (String)httpRequest.getAttribute(USER_PARAM); + assertNotNull(user); + assertEquals(user, httpRequest.getAttribute(KerberosPlugin.IMPERSONATOR_USER_NAME)); + } + } + } + + @AfterClass + public static void shutdown() throws Exception { + if (miniCluster != null) { + miniCluster.shutdown(); + } + miniCluster = null; + solrClient.close(); + solrClient = null; + System.clearProperty("authenticationPlugin"); + System.clearProperty(KerberosPlugin.DELEGATION_TOKEN_ENABLED); + System.clearProperty("solr.kerberos.cookie.domain"); + Map impSettings = getImpersonatorSettings(); + for (Map.Entry entry : impSettings.entrySet()) { + System.clearProperty(entry.getKey()); + } + System.clearProperty("solr.test.sys.prop1"); + System.clearProperty("solr.test.sys.prop2"); + SolrRequestParsers.DEFAULT.setAddRequestHeadersToContext(false); + } + + private void create1ShardCollection(String name, String config, MiniSolrCloudCluster solrCluster) throws Exception { + CollectionAdminResponse response; + CollectionAdminRequest.Create create = new CollectionAdminRequest.Create() { + @Override + public SolrParams getParams() { + ModifiableSolrParams msp = new ModifiableSolrParams(super.getParams()); + msp.set(USER_PARAM, "user"); + return msp; + } + }; + create.setConfigName(config); + create.setCollectionName(name); + create.setNumShards(1); + create.setReplicationFactor(1); + create.setMaxShardsPerNode(1); + response = create.process(solrCluster.getSolrClient()); + + if (response.getStatus() != 0 || response.getErrorMessages() != null) { + fail("Could not create collection. Response" + response.toString()); + } + ZkStateReader zkStateReader = solrCluster.getSolrClient().getZkStateReader(); + AbstractDistribZkTestBase.waitForRecoveriesToFinish(name, zkStateReader, false, true, 100); + } + + private SolrRequest getProxyRequest(String user, String doAs) { + return getProxyRequest(user, doAs, null); + } + + private SolrRequest getProxyRequest(String user, String doAs, String remoteHost) { + return getProxyRequest(user, doAs, remoteHost, null); + } + + private SolrRequest getProxyRequest(String user, String doAs, String remoteHost, String remoteAddress) { + return new CollectionAdminRequest.List() { + @Override + public SolrParams getParams() { + ModifiableSolrParams params = new ModifiableSolrParams(super.getParams()); + params.set(USER_PARAM, user); + params.set(KerberosPlugin.IMPERSONATOR_DO_AS_HTTP_PARAM, doAs); + if (remoteHost != null) params.set(REMOTE_HOST_PARAM, remoteHost); + if (remoteAddress != null) params.set(REMOTE_ADDRESS_PARAM, remoteAddress); + return params; + } + }; + } + + private String getExpectedGroupExMsg(String user, String doAs) { + return "User: " + user + " is not allowed to impersonate " + doAs; + } + + private String getExpectedHostExMsg(String user) { + return "Unauthorized connection for super-user: " + user; + } + + @Test + public void testProxyNoConfigGroups() throws Exception { + try { + solrClient.request(getProxyRequest("noGroups","bar")); + fail("Expected RemoteSolrException"); + } + catch (HttpSolrClient.RemoteSolrException ex) { + assertTrue(ex.getMessage().contains(getExpectedGroupExMsg("noGroups", "bar"))); + } + } + + @Test + public void testProxyWrongHost() throws Exception { + try { + solrClient.request(getProxyRequest("wrongHost","bar")); + fail("Expected RemoteSolrException"); + } + catch (HttpSolrClient.RemoteSolrException ex) { + assertTrue(ex.getMessage().contains(getExpectedHostExMsg("wrongHost"))); + } + } + + @Test + public void testProxyNoConfigHosts() throws Exception { + try { + solrClient.request(getProxyRequest("noHosts","bar")); + fail("Expected RemoteSolrException"); + } + catch (HttpSolrClient.RemoteSolrException ex) { + // FixMe: this should return an exception about the host being invalid, + // but a bug (HADOOP-11077) causes an NPE instead. + //assertTrue(ex.getMessage().contains(getExpectedHostExMsg("noHosts"))); + } + } + + @Test + public void testProxyValidateAnyHostAnyUser() throws Exception { + solrClient.request(getProxyRequest("anyHostAnyUser", "bar", null)); + assertTrue(ImpersonatorCollectionsHandler.called.get()); + } + + @Test + public void testProxyInvalidProxyUser() throws Exception { + try { + // wrong direction, should fail + solrClient.request(getProxyRequest("bar","anyHostAnyUser")); + fail("Expected RemoteSolrException"); + } + catch (HttpSolrClient.RemoteSolrException ex) { + assertTrue(ex.getMessage().contains(getExpectedGroupExMsg("bar", "anyHostAnyUser"))); + } + } + + @Test + public void testProxyValidateHost() throws Exception { + solrClient.request(getProxyRequest("localHostAnyGroup", "bar")); + assertTrue(ImpersonatorCollectionsHandler.called.get()); + } + + + + @Test + public void testProxyValidateGroup() throws Exception { + solrClient.request(getProxyRequest("anyHostUsersGroup", System.getProperty("user.name"), null)); + assertTrue(ImpersonatorCollectionsHandler.called.get()); + } + + @Test + public void testProxyUnknownRemote() throws Exception { + try { + // Use a reserved ip address + String nonProxyUserConfiguredIpAddress = "255.255.255.255"; + solrClient.request(getProxyRequest("localHostAnyGroup", "bar", "unknownhost.bar.foo", nonProxyUserConfiguredIpAddress)); + fail("Expected RemoteSolrException"); + } + catch (HttpSolrClient.RemoteSolrException ex) { + assertTrue(ex.getMessage().contains(getExpectedHostExMsg("localHostAnyGroup"))); + } + } + + @Test + public void testProxyInvalidRemote() throws Exception { + try { + String invalidIpAddress = "-127.-128"; + solrClient.request(getProxyRequest("localHostAnyGroup","bar", "[ff01::114]", invalidIpAddress)); + fail("Expected RemoteSolrException"); + } + catch (HttpSolrClient.RemoteSolrException ex) { + assertTrue(ex.getMessage().contains(getExpectedHostExMsg("localHostAnyGroup"))); + } + } + + @Test + public void testProxyInvalidGroup() throws Exception { + try { + solrClient.request(getProxyRequest("bogusGroup","bar", null)); + fail("Expected RemoteSolrException"); + } + catch (HttpSolrClient.RemoteSolrException ex) { + assertTrue(ex.getMessage().contains(getExpectedGroupExMsg("bogusGroup", "bar"))); + } + } + + @Test + public void testProxyNullProxyUser() throws Exception { + try { + solrClient.request(getProxyRequest("","bar")); + fail("Expected RemoteSolrException"); + } + catch (HttpSolrClient.RemoteSolrException ex) { + // this exception is specific to our implementation, don't check a specific message. + } + } + + @Test + public void testForwarding() throws Exception { + String collectionName = "forwardingCollection"; + File configDir = getFile("solr").toPath().resolve("collection1/conf").toFile(); + miniCluster.uploadConfigDir(configDir, "conf1"); + create1ShardCollection(collectionName, "conf1", miniCluster); + + // try a command to each node, one of them must be forwarded + for (JettySolrRunner jetty : miniCluster.getJettySolrRunners()) { + HttpSolrClient client = + new HttpSolrClient.Builder(jetty.getBaseUrl().toString() + "/" + collectionName).build(); + try { + ModifiableSolrParams params = new ModifiableSolrParams(); + params.set("q", "*:*"); + params.set(USER_PARAM, "user"); + client.query(params); + } finally { + client.close(); + } + } + } +} diff --git a/solr/core/src/test/org/apache/solr/security/HttpParamDelegationTokenAuthenticationHandler.java b/solr/core/src/test/org/apache/solr/security/HttpParamDelegationTokenAuthenticationHandler.java deleted file mode 100644 index 7c5c94a0444..00000000000 --- a/solr/core/src/test/org/apache/solr/security/HttpParamDelegationTokenAuthenticationHandler.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You under the Apache License, Version 2.0 - * (the "License"); you may not use this file except in compliance with - * the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ -package org.apache.solr.security; - -import java.io.IOException; -import java.nio.charset.Charset; -import java.util.List; -import java.util.Map; -import java.util.Properties; - -import javax.servlet.ServletException; -import javax.servlet.http.HttpServletRequest; -import javax.servlet.http.HttpServletResponse; - -import org.apache.hadoop.security.authentication.client.AuthenticationException; -import org.apache.hadoop.security.authentication.server.AuthenticationHandler; -import org.apache.hadoop.security.authentication.server.AuthenticationToken; -import org.apache.hadoop.security.token.delegation.web.DelegationTokenAuthenticationHandler; - -import org.apache.http.NameValuePair; -import org.apache.http.client.utils.URLEncodedUtils; - -/** - * AuthenticationHandler that supports delegation tokens and simple - * authentication via the "user" http parameter - */ -public class HttpParamDelegationTokenAuthenticationHandler extends - DelegationTokenAuthenticationHandler { - - public static final String USER_PARAM = "user"; - - public HttpParamDelegationTokenAuthenticationHandler() { - super(new HttpParamAuthenticationHandler()); - } - - @Override - public void init(Properties config) throws ServletException { - Properties conf = new Properties(); - for (Map.Entry entry : config.entrySet()) { - conf.setProperty((String) entry.getKey(), (String) entry.getValue()); - } - conf.setProperty(TOKEN_KIND, KerberosPlugin.DELEGATION_TOKEN_TYPE_DEFAULT); - super.init(conf); - } - - private static String getHttpParam(HttpServletRequest request, String param) { - List pairs = - URLEncodedUtils.parse(request.getQueryString(), Charset.forName("UTF-8")); - for (NameValuePair nvp : pairs) { - if(param.equals(nvp.getName())) { - return nvp.getValue(); - } - } - return null; - } - - private static class HttpParamAuthenticationHandler - implements AuthenticationHandler { - - @Override - public String getType() { - return "dummy"; - } - - @Override - public void init(Properties config) throws ServletException { - } - - @Override - public void destroy() { - } - - @Override - public boolean managementOperation(AuthenticationToken token, - HttpServletRequest request, HttpServletResponse response) - throws IOException, AuthenticationException { - return false; - } - - @Override - public AuthenticationToken authenticate(HttpServletRequest request, - HttpServletResponse response) - throws IOException, AuthenticationException { - AuthenticationToken token = null; - String userName = getHttpParam(request, USER_PARAM); - if (userName != null) { - return new AuthenticationToken(userName, userName, "test"); - } else { - response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); - response.setHeader("WWW-Authenticate", "dummy"); - } - return token; - } - } -} diff --git a/solr/core/src/test/org/apache/solr/security/HttpParamDelegationTokenPlugin.java b/solr/core/src/test/org/apache/solr/security/HttpParamDelegationTokenPlugin.java new file mode 100644 index 00000000000..7a4f69fa903 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/security/HttpParamDelegationTokenPlugin.java @@ -0,0 +1,272 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.solr.security; + +import java.io.IOException; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.security.Principal; +import java.util.Enumeration; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.Properties; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletContext; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; + +import org.apache.hadoop.security.authentication.client.AuthenticationException; +import org.apache.hadoop.security.authentication.server.AuthenticationFilter; +import org.apache.hadoop.security.authentication.server.AuthenticationHandler; +import org.apache.hadoop.security.authentication.server.AuthenticationToken; +import org.apache.hadoop.security.token.delegation.web.DelegationTokenAuthenticationHandler; + +import org.apache.http.HttpException; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.NameValuePair; +import org.apache.http.client.utils.URLEncodedUtils; +import org.apache.http.protocol.HttpContext; +import org.apache.solr.client.solrj.impl.HttpClientUtil; +import org.apache.solr.client.solrj.impl.SolrHttpClientBuilder; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.util.ExecutorUtil; +import org.apache.solr.core.CoreContainer; +import org.apache.solr.request.SolrRequestInfo; + +/** + * AuthenticationHandler that supports delegation tokens and simple + * authentication via the "user" http parameter + */ +public class HttpParamDelegationTokenPlugin extends KerberosPlugin { + public static final String USER_PARAM = "user"; // http parameter for user authentication + public static final String REMOTE_HOST_PARAM = "remoteHost"; // http parameter for indicating remote host + public static final String REMOTE_ADDRESS_PARAM = "remoteAddress"; // http parameter for indicating remote address + public static final String INTERNAL_REQUEST_HEADER = "internalRequest"; // http header for indicating internal request + + boolean isSolrThread() { + return ExecutorUtil.isSolrServerThread(); + } + + private final HttpRequestInterceptor interceptor = new HttpRequestInterceptor() { + @Override + public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException { + SolrRequestInfo reqInfo = SolrRequestInfo.getRequestInfo(); + String usr; + if (reqInfo != null) { + Principal principal = reqInfo.getReq().getUserPrincipal(); + if (principal == null) { + //this had a request but not authenticated + //so we don't not need to set a principal + return; + } else { + usr = principal.getName(); + } + } else { + if (!isSolrThread()) { + //if this is not running inside a Solr threadpool (as in testcases) + // then no need to add any header + return; + } + //this request seems to be originated from Solr itself + usr = "$"; //special name to denote the user is the node itself + } + httpRequest.setHeader(INTERNAL_REQUEST_HEADER, usr); + } + }; + + public HttpParamDelegationTokenPlugin(CoreContainer coreContainer) { + super(coreContainer); + } + + @Override + public void init(Map pluginConfig) { + try { + final FilterConfig initConf = getInitFilterConfig(pluginConfig, true); + + FilterConfig conf = new FilterConfig() { + @Override + public ServletContext getServletContext() { + return initConf.getServletContext(); + } + + @Override + public Enumeration getInitParameterNames() { + return initConf.getInitParameterNames(); + } + + @Override + public String getInitParameter(String param) { + if (AuthenticationFilter.AUTH_TYPE.equals(param)) { + return HttpParamDelegationTokenAuthenticationHandler.class.getName(); + } + return initConf.getInitParameter(param); + } + + @Override + public String getFilterName() { + return "HttpParamFilter"; + } + }; + Filter kerberosFilter = new HttpParamToRequestFilter(); + kerberosFilter.init(conf); + setKerberosFilter(kerberosFilter); + } catch (ServletException e) { + throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, + "Error initializing kerberos authentication plugin: "+e); + } + } + + @Override + public SolrHttpClientBuilder getHttpClientBuilder(SolrHttpClientBuilder builder) { + HttpClientUtil.addRequestInterceptor(interceptor); + return super.getHttpClientBuilder(builder); + } + + @Override + public void close() { + HttpClientUtil.removeRequestInterceptor(interceptor); + super.close(); + } + + private static String getHttpParam(HttpServletRequest request, String param) { + List pairs = URLEncodedUtils.parse(request.getQueryString(), Charset.forName("UTF-8")); + for (NameValuePair nvp : pairs) { + if (param.equals(nvp.getName())) { + return nvp.getValue(); + } + } + return null; + } + + public static class HttpParamDelegationTokenAuthenticationHandler extends + DelegationTokenAuthenticationHandler { + + public HttpParamDelegationTokenAuthenticationHandler() { + super(new HttpParamAuthenticationHandler()); + } + + @Override + public void init(Properties config) throws ServletException { + Properties conf = new Properties(); + for (Map.Entry entry : config.entrySet()) { + conf.setProperty((String) entry.getKey(), (String) entry.getValue()); + } + conf.setProperty(TOKEN_KIND, KerberosPlugin.DELEGATION_TOKEN_TYPE_DEFAULT); + super.init(conf); + } + + private static class HttpParamAuthenticationHandler implements AuthenticationHandler { + @Override + public String getType() { + return "dummy"; + } + + @Override + public void init(Properties config) throws ServletException { + } + + @Override + public void destroy() { + } + + @Override + public boolean managementOperation(AuthenticationToken token, + HttpServletRequest request, HttpServletResponse response) + throws IOException, AuthenticationException { + return false; + } + + @Override + public AuthenticationToken authenticate(HttpServletRequest request, + HttpServletResponse response) + throws IOException, AuthenticationException { + AuthenticationToken token = null; + String userName = getHttpParam(request, USER_PARAM); + if (userName == null) { + //check if this is an internal request + userName = request.getHeader(INTERNAL_REQUEST_HEADER); + } + if (userName != null) { + return new AuthenticationToken(userName, userName, "test"); + } else { + response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); + response.setHeader("WWW-Authenticate", "dummy"); + } + return token; + } + } + } + + /** + * Filter that converts http params to HttpServletRequest params + */ + private static class HttpParamToRequestFilter extends DelegationTokenKerberosFilter { + @Override + public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) + throws IOException, ServletException { + final HttpServletRequest httpRequest = (HttpServletRequest) request; + final HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper(httpRequest) { + @Override + public String getRemoteHost() { + String param = getHttpParam(httpRequest, REMOTE_HOST_PARAM); + return param != null ? param : httpRequest.getRemoteHost(); + } + + @Override + public String getRemoteAddr() { + String param = getHttpParam(httpRequest, REMOTE_ADDRESS_PARAM); + return param != null ? param : httpRequest.getRemoteAddr(); + } + }; + + super.doFilter(requestWrapper, response, chain); + } + + @Override + protected void doFilter(FilterChain filterChain, HttpServletRequest request, + HttpServletResponse response) throws IOException, ServletException { + // remove the filter-specific authentication information, so it doesn't get accidentally forwarded. + List newPairs = new LinkedList(); + List pairs = URLEncodedUtils.parse(request.getQueryString(), Charset.forName("UTF-8")); + for (NameValuePair nvp : pairs) { + if (!USER_PARAM.equals(nvp.getName())) { + newPairs.add(nvp); + } + else { + request.setAttribute(USER_PARAM, nvp.getValue()); + } + } + final String queryStringNoUser = URLEncodedUtils.format(newPairs, StandardCharsets.UTF_8); + HttpServletRequest requestWrapper = new HttpServletRequestWrapper(request) { + @Override + public String getQueryString() { + return queryStringNoUser; + } + }; + super.doFilter(filterChain, requestWrapper, response); + } + } +}