From bd430506ce094f1593933146c52557f1822cf47e Mon Sep 17 00:00:00 2001 From: Noble Paul Date: Thu, 6 Aug 2015 19:03:25 +0000 Subject: [PATCH] SOLR-7837: An AuthenticationPlugin which implements the HTTP BasicAuth protocol and stores credentials securely in ZooKeeper git-svn-id: https://svn.apache.org/repos/asf/lucene/dev/trunk@1694555 13f79535-47bb-0310-9956-ffa450edef68 --- solr/CHANGES.txt | 3 + .../apache/solr/security/BasicAuthPlugin.java | 150 +++++++++++ .../Sha256AuthenticationProvider.java | 152 +++++++++++ .../solr/cloud/TestMiniSolrCloudCluster.java | 20 +- .../admin/SecurityConfHandlerTest.java | 236 ++++++++++++++++++ .../security/BasicAuthIntegrationTest.java | 210 ++++++++++++++++ .../TestSha256AuthenticationProvider.java | 46 ++++ 7 files changed, 809 insertions(+), 8 deletions(-) create mode 100644 solr/core/src/java/org/apache/solr/security/BasicAuthPlugin.java create mode 100644 solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java create mode 100644 solr/core/src/test/org/apache/solr/handler/admin/SecurityConfHandlerTest.java create mode 100644 solr/core/src/test/org/apache/solr/security/BasicAuthIntegrationTest.java create mode 100644 solr/core/src/test/org/apache/solr/security/TestSha256AuthenticationProvider.java diff --git a/solr/CHANGES.txt b/solr/CHANGES.txt index 45ae5ecb7bf..f6aab5132ec 100644 --- a/solr/CHANGES.txt +++ b/solr/CHANGES.txt @@ -201,6 +201,9 @@ New Features * SOLR-7838: An authorizationPlugin interface where the access control rules are stored/managed in ZooKeeper (Noble Paul, Anshum Gupta, Ishan Chattopadhyaya) +* SOLR-7837: An AuthenticationPlugin which implements the HTTP BasicAuth protocol and stores credentials + securely in ZooKeeper (Noble Paul, Anshum Gupta,Ishan Chattopadhyaya) + Bug Fixes ---------------------- diff --git a/solr/core/src/java/org/apache/solr/security/BasicAuthPlugin.java b/solr/core/src/java/org/apache/solr/security/BasicAuthPlugin.java new file mode 100644 index 00000000000..384b438cb3e --- /dev/null +++ b/solr/core/src/java/org/apache/solr/security/BasicAuthPlugin.java @@ -0,0 +1,150 @@ +package org.apache.solr.security; + +/* + * 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. + */ + + +import javax.servlet.FilterChain; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletRequestWrapper; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.security.Principal; +import java.util.List; +import java.util.Map; +import java.util.StringTokenizer; + +import org.apache.commons.codec.binary.Base64; +import org.apache.http.Header; +import org.apache.http.HttpException; +import org.apache.http.HttpRequest; +import org.apache.http.HttpRequestInterceptor; +import org.apache.http.auth.BasicUserPrincipal; +import org.apache.http.impl.client.DefaultHttpClient; +import org.apache.http.message.BasicHeader; +import org.apache.http.protocol.HttpContext; +import org.apache.solr.client.solrj.impl.HttpClientConfigurer; +import org.apache.solr.common.SolrException; +import org.apache.solr.common.params.SolrParams; +import org.apache.solr.util.CommandOperation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class BasicAuthPlugin extends AuthenticationPlugin implements ConfigEditablePlugin { + protected static final Logger log = LoggerFactory.getLogger(BasicAuthPlugin.class); + private AuthenticationProvider zkAuthentication; + private final static ThreadLocal
authHeader = new ThreadLocal<>(); + + public boolean authenticate(String username, String pwd) { + return zkAuthentication.authenticate(username, pwd); + } + + @Override + public void init(Map pluginConfig) { + zkAuthentication = getAuthenticationProvider(pluginConfig); + } + + @Override + public Map edit(Map latestConf, List commands) { + if (zkAuthentication instanceof ConfigEditablePlugin) { + ConfigEditablePlugin editablePlugin = (ConfigEditablePlugin) zkAuthentication; + return editablePlugin.edit(latestConf, commands); + } + throw new SolrException(SolrException.ErrorCode.BAD_REQUEST, "This cannot be edited"); + } + + protected AuthenticationProvider getAuthenticationProvider(Map pluginConfig) { + Sha256AuthenticationProvider provider = new Sha256AuthenticationProvider(); + provider.init(pluginConfig); + return provider; + } + + private void authenticationFailure(HttpServletResponse response, String message) throws IOException { + for (Map.Entry entry : zkAuthentication.getPromptHeaders().entrySet()) { + response.setHeader(entry.getKey(), entry.getValue()); + } + response.sendError(401, message); + } + + @Override + public void doAuthenticate(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws Exception { + + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + String authHeader = request.getHeader("Authorization"); + if (authHeader != null) { + BasicAuthPlugin.authHeader.set(new BasicHeader("Authorization", authHeader)); + StringTokenizer st = new StringTokenizer(authHeader); + if (st.hasMoreTokens()) { + String basic = st.nextToken(); + if (basic.equalsIgnoreCase("Basic")) { + try { + String credentials = new String(Base64.decodeBase64(st.nextToken()), "UTF-8"); + int p = credentials.indexOf(":"); + if (p != -1) { + final String username = credentials.substring(0, p).trim(); + String pwd = credentials.substring(p + 1).trim(); + if (!authenticate(username, pwd)) { + authenticationFailure(response, "Bad credentials"); + } else { + HttpServletRequestWrapper wrapper = new HttpServletRequestWrapper(request) { + @Override + public Principal getUserPrincipal() { + return new BasicUserPrincipal(username); + } + }; + filterChain.doFilter(wrapper, response); + } + + } else { + authenticationFailure(response, "Invalid authentication token"); + } + } catch (UnsupportedEncodingException e) { + throw new Error("Couldn't retrieve authentication", e); + } + } + } + } else { + request.setAttribute(AuthenticationPlugin.class.getName(), zkAuthentication.getPromptHeaders()); + filterChain.doFilter(request, response); + } + } + + @Override + public void close() throws IOException { + + } + + @Override + public void closeRequest() { + authHeader.remove(); + } + + public interface AuthenticationProvider { + void init(Map pluginConfig); + + boolean authenticate(String user, String pwd); + + Map getPromptHeaders(); + } + + +} diff --git a/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java b/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java new file mode 100644 index 00000000000..b38b01bae92 --- /dev/null +++ b/solr/core/src/java/org/apache/solr/security/Sha256AuthenticationProvider.java @@ -0,0 +1,152 @@ +package org.apache.solr.security; + +/* + * 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. + */ + +import java.nio.charset.StandardCharsets; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.security.SecureRandom; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.Set; + +import com.google.common.collect.ImmutableSet; +import org.apache.commons.codec.binary.Base64; +import org.apache.solr.util.CommandOperation; + +import static org.apache.solr.handler.admin.SecurityConfHandler.getMapValue; + +public class Sha256AuthenticationProvider implements ConfigEditablePlugin, BasicAuthPlugin.AuthenticationProvider { + private Map credentials; + private String realm; + private Map promptHeader; + + + static void putUser(String user, String pwd, Map credentials) { + if (user == null || pwd == null) return; + + final Random r = new SecureRandom(); + byte[] salt = new byte[32]; + r.nextBytes(salt); + String saltBase64 = Base64.encodeBase64String(salt); + String val = sha256(pwd, saltBase64) + " " + saltBase64; + credentials.put(user, val); + } + + @Override + public void init(Map pluginConfig) { + if (pluginConfig.get("realm") != null) this.realm = (String) pluginConfig.get("realm"); + else this.realm = "solr"; + + promptHeader = Collections.unmodifiableMap(Collections.singletonMap("WWW-Authenticate", "Basic realm=\"" + realm + "\"")); + credentials = new LinkedHashMap<>(); + Map users = (Map) pluginConfig.get("credentials"); + if (users == null) { + BasicAuthPlugin.log.warn("No users configured yet"); + return; + } + for (Map.Entry e : users.entrySet()) { + String v = e.getValue(); + if (v == null) { + BasicAuthPlugin.log.warn("user has no password " + e.getKey()); + continue; + } + credentials.put(e.getKey(), v); + } + + } + + public boolean authenticate(String username, String password) { + String cred = credentials.get(username); + if (cred == null || cred.isEmpty()) return false; + cred = cred.trim(); + String salt = null; + if (cred.contains(" ")) { + String[] ss = cred.split(" "); + if (ss.length > 1 && !ss[1].isEmpty()) { + salt = ss[1]; + cred = ss[0]; + } + } + return cred.equals(sha256(password, salt)); + } + + @Override + public Map getPromptHeaders() { + return promptHeader; + } + + public static String sha256(String password, String saltKey) { + MessageDigest digest; + try { + digest = MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + BasicAuthPlugin.log.error(e.getMessage(), e); + return null;//should not happen + } + if (saltKey != null) { + digest.reset(); + digest.update(Base64.decodeBase64(saltKey)); + } + + byte[] btPass = digest.digest(password.getBytes(StandardCharsets.UTF_8)); + digest.reset(); + btPass = digest.digest(btPass); + return Base64.encodeBase64String(btPass); + } + + @Override + public Map edit(Map latestConf, List commands) { + for (CommandOperation cmd : commands) { + if (!supported_ops.contains(cmd.name)) { + cmd.unknownOperation(); + return null; + } + if (cmd.hasError()) return null; + if ("delete-user".equals(cmd.name)) { + List names = cmd.getStrs(""); + Map map = (Map) latestConf.get("credentials"); + if (map == null || !map.keySet().containsAll(names)) { + cmd.addError("No such user(s) " +names ); + return null; + } + for (String name : names) map.remove(name); + return latestConf; + } + if ("set-user".equals(cmd.name) ) { + Map map = getMapValue(latestConf, "credentials"); + Map kv = cmd.getDataMap(); + for (Object o : kv.entrySet()) { + Map.Entry e = (Map.Entry) o; + if(e.getKey() == null || e.getValue() == null){ + cmd.addError("name and password must be non-null"); + return null; + } + putUser(String.valueOf(e.getKey()), String.valueOf(e.getValue()), map); + } + + } + } + return latestConf; + } + + static final Set supported_ops = ImmutableSet.of("set-user", "delete-user"); +} diff --git a/solr/core/src/test/org/apache/solr/cloud/TestMiniSolrCloudCluster.java b/solr/core/src/test/org/apache/solr/cloud/TestMiniSolrCloudCluster.java index 7aafaedaede..686bca4901e 100644 --- a/solr/core/src/test/org/apache/solr/cloud/TestMiniSolrCloudCluster.java +++ b/solr/core/src/test/org/apache/solr/cloud/TestMiniSolrCloudCluster.java @@ -17,6 +17,14 @@ package org.apache.solr.cloud; * limitations under the License. */ +import java.io.File; +import java.io.IOException; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + import com.carrotsearch.randomizedtesting.rules.SystemPropertiesRestoreRule; import org.apache.lucene.util.LuceneTestCase; import org.apache.lucene.util.LuceneTestCase.SuppressSysoutChecks; @@ -45,14 +53,6 @@ import org.junit.rules.TestRule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -import java.io.File; -import java.io.IOException; -import java.util.Collection; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.concurrent.atomic.AtomicInteger; - /** * Test of the MiniSolrCloudCluster functionality. Keep in mind, * MiniSolrCloudCluster is designed to be used outside of the Lucene test @@ -205,6 +205,7 @@ public class TestMiniSolrCloudCluster extends LuceneTestCase { assertTrue(e.code() >= 500 && e.code() < 600); } + doExtraTests(miniCluster, zkClient, zkStateReader,cloudSolrClient, collectionName); // delete the collection we created earlier miniCluster.deleteCollection(collectionName); AbstractDistribZkTestBase.waitForCollectionToDisappear(collectionName, zkStateReader, true, true, 330); @@ -215,6 +216,9 @@ public class TestMiniSolrCloudCluster extends LuceneTestCase { } } + protected void doExtraTests(MiniSolrCloudCluster miniCluster, SolrZkClient zkClient, ZkStateReader zkStateReader, CloudSolrClient cloudSolrClient, + String defaultCollName) throws Exception { /*do nothing*/ } + @Test public void testErrorsInStartup() throws Exception { diff --git a/solr/core/src/test/org/apache/solr/handler/admin/SecurityConfHandlerTest.java b/solr/core/src/test/org/apache/solr/handler/admin/SecurityConfHandlerTest.java new file mode 100644 index 00000000000..041d9b5d522 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/handler/admin/SecurityConfHandlerTest.java @@ -0,0 +1,236 @@ +package org.apache.solr.handler.admin; + +/* + * 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. + */ + + +import java.nio.charset.StandardCharsets; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.common.cloud.ZkStateReader.ConfigData; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.util.ContentStreamBase; +import org.apache.solr.common.util.Utils; +import org.apache.solr.request.LocalSolrQueryRequest; +import org.apache.solr.response.SolrQueryResponse; +import org.apache.solr.security.BasicAuthPlugin; +import org.apache.solr.security.RuleBasedAuthorizationPlugin; +import org.apache.solr.util.CommandOperation; + +import static org.apache.solr.common.util.Utils.makeMap; + +public class SecurityConfHandlerTest extends SolrTestCaseJ4 { + + public void testEdit() throws Exception { + MockSecurityHandler handler = new MockSecurityHandler(); + String command = "{\n" + + "'set-user': {'tom':'TomIsCool'},\n" + + "'set-user':{ 'tom':'TomIsUberCool'}\n" + + "}"; + LocalSolrQueryRequest req = new LocalSolrQueryRequest(null, new ModifiableSolrParams()); + req.getContext().put("httpMethod","POST"); + req.getContext().put("path","/admin/authentication"); + ContentStreamBase.ByteArrayStream o = new ContentStreamBase.ByteArrayStream(command.getBytes(StandardCharsets.UTF_8),""); + req.setContentStreams(Collections.singletonList(o)); + handler.handleRequestBody(req,new SolrQueryResponse()); + + BasicAuthPlugin basicAuth = new BasicAuthPlugin(); + ConfigData securityCfg = (ConfigData) handler.m.get("/security.json"); + basicAuth.init((Map) securityCfg.data.get("authentication")); + assertTrue(basicAuth.authenticate("tom", "TomIsUberCool")); + + command = "{\n" + + "'set-user': {'harry':'HarryIsCool'},\n" + + "'delete-user': ['tom','harry']\n" + + "}"; + o = new ContentStreamBase.ByteArrayStream(command.getBytes(StandardCharsets.UTF_8),""); + req.setContentStreams(Collections.singletonList(o)); + handler.handleRequestBody(req,new SolrQueryResponse()); + securityCfg = (ConfigData) handler.m.get("/security.json"); + assertEquals(3, securityCfg.version); + Map result = (Map) securityCfg.data.get("authentication"); + result = (Map) result.get("credentials"); + assertTrue(result.isEmpty()); + + + + command = "{'set-user-role': { 'tom': ['admin','dev']},\n" + + "'set-permission':{'name': 'security-edit',\n" + + " 'role': 'admin'\n" + + " },\n" + + "'set-permission':{'name':'some-permission',\n" + + " 'collection':'acoll',\n" + + " 'path':'/nonexistentpath',\n" + + " 'role':'guest',\n" + + " 'before':'security-edit'\n" + + " }\n" + + "}"; + + req = new LocalSolrQueryRequest(null, new ModifiableSolrParams()); + req.getContext().put("httpMethod","POST"); + req.getContext().put("path","/admin/authorization"); + o = new ContentStreamBase.ByteArrayStream(command.getBytes(StandardCharsets.UTF_8),""); + req.setContentStreams(Collections.singletonList(o)); + SolrQueryResponse rsp = new SolrQueryResponse(); + handler.handleRequestBody(req, rsp); + assertNull(rsp.getValues().get(CommandOperation.ERR_MSGS)); + Map authzconf = (Map) ((ConfigData) handler.m.get("/security.json")).data.get("authorization"); + Map userRoles = (Map) authzconf.get("user-role"); + List tomRoles = (List) userRoles.get("tom"); + assertTrue(tomRoles.contains("admin")); + assertTrue(tomRoles.contains("dev")); + Map permissions = (Map) authzconf.get("permissions"); + assertEquals(2, permissions.size()); + for (Object p : permissions.entrySet()) { + Map.Entry e = (Map.Entry) p; + assertEquals("some-permission", e.getKey()); + break; + } + command = "{\n" + + "'delete-permission': 'some-permission',\n" + + "'set-user-role':{'tom':null}\n" + + "}"; + req = new LocalSolrQueryRequest(null, new ModifiableSolrParams()); + req.getContext().put("httpMethod","POST"); + req.getContext().put("path","/admin/authorization"); + o = new ContentStreamBase.ByteArrayStream(command.getBytes(StandardCharsets.UTF_8),""); + req.setContentStreams(Collections.singletonList(o)); + rsp = new SolrQueryResponse(); + handler.handleRequestBody(req, rsp); + assertNull(rsp.getValues().get(CommandOperation.ERR_MSGS)); + authzconf = (Map) ((ConfigData) handler.m.get("/security.json")).data.get("authorization"); + userRoles = (Map) authzconf.get("user-role"); + assertEquals(0, userRoles.size()); + permissions = (Map) authzconf.get("permissions"); + assertEquals(1, permissions.size()); + assertNull(permissions.get("some-permission")); + command = "{\n" + + "'set-permission':{'name': 'security-edit',\n" + + " 'method':'POST',"+ // security edit is a well-known permission , only role attribute should be provided + " 'role': 'admin'\n" + + " }}"; + req = new LocalSolrQueryRequest(null, new ModifiableSolrParams()); + req.getContext().put("httpMethod","POST"); + req.getContext().put("path","/admin/authorization"); + o = new ContentStreamBase.ByteArrayStream(command.getBytes(StandardCharsets.UTF_8),""); + req.setContentStreams(Collections.singletonList(o)); + rsp = new SolrQueryResponse(); + handler.handleRequestBody(req, rsp); + List l = (List) ((Map) ((List)rsp.getValues().get("errorMessages")).get(0)).get("errorMessages"); + assertEquals(1, l.size()); + + } + + + public static class MockSecurityHandler extends SecurityConfHandler { + private Map m; + final BasicAuthPlugin basicAuthPlugin = new BasicAuthPlugin(); + final RuleBasedAuthorizationPlugin rulesBasedAuthorizationPlugin = new RuleBasedAuthorizationPlugin(); + + + public MockSecurityHandler() { + super(null); + m = new HashMap<>(); + ConfigData data = new ConfigData(makeMap("authentication", makeMap("class", "solr."+ BasicAuthPlugin.class.getSimpleName())), 1); + data.data.put("authorization", makeMap("class", "solr."+RuleBasedAuthorizationPlugin.class.getSimpleName())); + m.put("/security.json", data); + + + basicAuthPlugin.init(new HashMap<>()); + + rulesBasedAuthorizationPlugin.init(new HashMap<>()); + } + + public Map getM() { + return m; + } + + @Override + Object getPlugin(String key) { + if (key.equals("authentication")) { + return basicAuthPlugin; + } + if (key.equals("authorization")) { + return rulesBasedAuthorizationPlugin; + } + return null; + } + + @Override + ConfigData getSecurityProps(boolean getFresh) { + return (ConfigData) m.get("/security.json"); + } + + @Override + boolean persistConf(String key, byte[] buf, int version) { + Object data = m.get(key); + if (data instanceof ConfigData) { + ConfigData configData = (ConfigData) data; + if (configData.version == version) { + ConfigData result = new ConfigData((Map) Utils.fromJSON(buf), version + 1); + m.put(key, result); + return true; + } else { + return false; + } + } + throw new RuntimeException(); + } + + + public String getStandardJson() throws Exception { + String command = "{\n" + + "'set-user': {'solr':'SolrRocks'}\n" + + "}"; + LocalSolrQueryRequest req = new LocalSolrQueryRequest(null, new ModifiableSolrParams()); + req.getContext().put("httpMethod","POST"); + req.getContext().put("path","/admin/authentication"); + ContentStreamBase.ByteArrayStream o = new ContentStreamBase.ByteArrayStream(command.getBytes(StandardCharsets.UTF_8),""); + req.setContentStreams(Collections.singletonList(o)); + handleRequestBody(req, new SolrQueryResponse()); + + command = "{'set-user-role': { 'solr': 'admin'},\n" + + "'set-permission':{'name': 'security-edit', 'role': 'admin'}" + + "}"; + req = new LocalSolrQueryRequest(null, new ModifiableSolrParams()); + req.getContext().put("httpMethod","POST"); + req.getContext().put("path","/admin/authorization"); + o = new ContentStreamBase.ByteArrayStream(command.getBytes(StandardCharsets.UTF_8),""); + req.setContentStreams(Collections.singletonList(o)); + SolrQueryResponse rsp = new SolrQueryResponse(); + handleRequestBody(req, rsp); + Map data = ((ConfigData) m.get("/security.json")).data; + ((Map)data.get("authentication")).remove(""); + ((Map)data.get("authorization")).remove(""); + return Utils.toJSONString (data); + } + } + + + public static void main(String[] args) throws Exception{ + System.out.println(new MockSecurityHandler().getStandardJson()); + } + + + +} + + diff --git a/solr/core/src/test/org/apache/solr/security/BasicAuthIntegrationTest.java b/solr/core/src/test/org/apache/solr/security/BasicAuthIntegrationTest.java new file mode 100644 index 00000000000..fbefbe84ad2 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/security/BasicAuthIntegrationTest.java @@ -0,0 +1,210 @@ +package org.apache.solr.security; + +/* + * 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. + */ + + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Random; +import java.util.function.Predicate; + +import org.apache.http.HttpResponse; +import org.apache.http.client.HttpClient; +import org.apache.http.client.methods.HttpGet; +import org.apache.http.client.methods.HttpPost; +import org.apache.http.entity.ByteArrayEntity; +import org.apache.http.message.AbstractHttpMessage; +import org.apache.http.message.BasicHeader; +import org.apache.http.util.EntityUtils; +import org.apache.solr.client.solrj.SolrRequest; +import org.apache.solr.client.solrj.impl.CloudSolrClient; +import org.apache.solr.client.solrj.impl.HttpSolrClient; +import org.apache.solr.client.solrj.request.GenericSolrRequest; +import org.apache.solr.cloud.MiniSolrCloudCluster; +import org.apache.solr.cloud.TestMiniSolrCloudCluster; +import org.apache.solr.common.cloud.DocCollection; +import org.apache.solr.common.cloud.Replica; +import org.apache.solr.common.cloud.Slice; +import org.apache.solr.common.cloud.SolrZkClient; +import org.apache.solr.common.cloud.ZkStateReader; +import org.apache.solr.common.params.ModifiableSolrParams; +import org.apache.solr.common.util.Base64; +import org.apache.solr.common.util.ContentStreamBase; +import org.apache.solr.common.util.NamedList; +import org.apache.solr.common.util.StrUtils; +import org.apache.solr.common.util.Utils; +import org.apache.solr.util.CommandOperation; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import static java.nio.charset.StandardCharsets.UTF_8; +import static java.util.Collections.singletonMap; +import static org.apache.solr.common.cloud.ZkStateReader.BASE_URL_PROP; + + +public class BasicAuthIntegrationTest extends TestMiniSolrCloudCluster { + + private static final Logger log = LoggerFactory.getLogger(BasicAuthIntegrationTest.class); + + + @Override + protected void doExtraTests(MiniSolrCloudCluster miniCluster, SolrZkClient zkClient, ZkStateReader zkStateReader, + CloudSolrClient cloudSolrClient, String defaultCollName) throws Exception { + + NamedList rsp = cloudSolrClient.request(new GenericSolrRequest(SolrRequest.METHOD.GET, "/admin/authentication", new ModifiableSolrParams())); + assertNotNull(rsp.get(CommandOperation.ERR_MSGS)); + zkClient.setData("/security.json", STD_CONF.replaceAll("'", "\"").getBytes(UTF_8), true); + String baseUrl = getRandomReplica(zkStateReader.getClusterState().getCollection(defaultCollName), random()).getStr(BASE_URL_PROP); + + HttpClient cl = cloudSolrClient.getLbClient().getHttpClient(); + verifySecurityStatus(cl, baseUrl + "/admin/authentication", "authentication/class", "solr.BasicAuthPlugin", 20); + + String command = "{\n" + + "'set-user': {'harry':'HarryIsCool'}\n" + + "}"; + + GenericSolrRequest genericReq = new GenericSolrRequest(SolrRequest.METHOD.POST, "/admin/authentication", new ModifiableSolrParams()); + genericReq.setContentStreams(Collections.singletonList(new ContentStreamBase.ByteArrayStream(command.getBytes(UTF_8), ""))); + try { + cloudSolrClient.request(genericReq); + fail("Should have failed with a 401"); + } catch (HttpSolrClient.RemoteSolrException e) { + } + command = "{\n" + + "'set-user': {'harry':'HarryIsUberCool'}\n" + + "}"; + + HttpPost httpPost = new HttpPost(baseUrl + "/admin/authentication"); + setBasicAuthHeader(httpPost, "solr", "SolrRocks"); + httpPost.setEntity(new ByteArrayEntity(command.getBytes(UTF_8))); + httpPost.addHeader("Content-Type", "application/json; charset=UTF-8"); + verifySecurityStatus(cl, baseUrl + "/admin/authentication", "authentication.enabled", "true", 20); + HttpResponse r = cl.execute(httpPost); + int statusCode = r.getStatusLine().getStatusCode(); + assertEquals("proper_cred sent, but access denied", 200, statusCode); + baseUrl = getRandomReplica(zkStateReader.getClusterState().getCollection(defaultCollName), random()).getStr(BASE_URL_PROP); + + verifySecurityStatus(cl, baseUrl + "/admin/authentication", "authentication/credentials/harry", NOT_NULL_PREDICATE, 20); + command = "{\n" + + "'set-user-role': {'harry':'admin'}\n" + + "}"; + + httpPost = new HttpPost(baseUrl + "/admin/authorization"); + setBasicAuthHeader(httpPost, "solr", "SolrRocks"); + httpPost.setEntity(new ByteArrayEntity(command.getBytes(UTF_8))); + httpPost.addHeader("Content-Type", "application/json; charset=UTF-8"); + r = cl.execute(httpPost); + assertEquals(200, r.getStatusLine().getStatusCode()); + + baseUrl = getRandomReplica(zkStateReader.getClusterState().getCollection(defaultCollName), random()).getStr(BASE_URL_PROP); + verifySecurityStatus(cl, baseUrl+"/admin/authorization", "authorization/user-role/harry", NOT_NULL_PREDICATE, 20); + + + httpPost = new HttpPost(baseUrl + "/admin/authorization"); + setBasicAuthHeader(httpPost, "harry", "HarryIsUberCool"); + httpPost.setEntity(new ByteArrayEntity(Utils.toJSON(singletonMap("set-permission", Utils.makeMap + ("name", "x-update", + "collection", "x", + "path", "/update/*", + "role", "dev"))))); + + httpPost.addHeader("Content-Type", "application/json; charset=UTF-8"); + verifySecurityStatus(cl, baseUrl + "/admin/authorization", "authorization/user-role/harry", NOT_NULL_PREDICATE, 20); + r = cl.execute(httpPost); + assertEquals(200, r.getStatusLine().getStatusCode()); + + verifySecurityStatus(cl, baseUrl+"/admin/authorization", "authorization/permissions/x-update/collection", "x", 20); + + } + + public static void verifySecurityStatus(HttpClient cl, String url, String objPath, Object expected, int count) throws Exception { + boolean success = false; + String s = null; + List hierarchy = StrUtils.splitSmart(objPath, '/'); + for (int i = 0; i < count; i++) { + HttpGet get = new HttpGet(url); + s = EntityUtils.toString(cl.execute(get).getEntity()); + Map m = (Map) Utils.fromJSONString(s); + + Object actual = Utils.getObjectByPath(m, true, hierarchy); + if (expected instanceof Predicate) { + Predicate predicate = (Predicate) expected; + if (predicate.test(actual)) { + success = true; + break; + } + } else if (Objects.equals(String.valueOf(actual), expected)) { + success = true; + break; + } + Thread.sleep(50); + } + assertTrue("No match for " + objPath + " = " + expected + ", full response = " + s, success); + + } + + public static void setBasicAuthHeader(AbstractHttpMessage httpMsg, String user, String pwd) { + String userPass = user + ":" + pwd; + String encoded = Base64.byteArrayToBase64(userPass.getBytes(UTF_8)); + httpMsg.setHeader(new BasicHeader("Authorization", "Basic " + encoded)); + log.info("Added Basic Auth security Header {}",encoded ); + } + + public static Replica getRandomReplica(DocCollection coll, Random random) { + ArrayList l = new ArrayList<>(); + + for (Slice slice : coll.getSlices()) { + for (Replica replica : slice.getReplicas()) { + l.add(replica); + } + } + Collections.shuffle(l, random); + return l.isEmpty() ? null : l.get(0); + } + + static final Predicate NOT_NULL_PREDICATE = new Predicate() { + @Override + public boolean test(Object o) { + return o != null; + } + }; + + + @Override + public void testErrorsInStartup() throws Exception { + //don't do anything + } + + @Override + public void testErrorsInShutdown() throws Exception { + } + + //the password is 'SolrRocks' + //this could be generated everytime. But , then we will not know if there is any regression + private static final String STD_CONF = "{\n" + + " 'authentication':{\n" + + " 'class':'solr.BasicAuthPlugin',\n" + + " 'credentials':{'solr':'orwp2Ghgj39lmnrZOTm7Qtre1VqHFDfwAEzr0ApbN3Y= Ju5osoAqOX8iafhWpPP01E5P+sg8tK8tHON7rCYZRRw='}},\n" + + " 'authorization':{\n" + + " 'class':'solr.RuleBasedAuthorizationPlugin',\n" + + " 'user-role':{'solr':'admin'},\n" + + " 'permissions':{'security-edit':{'role':'admin'}}}}"; +} diff --git a/solr/core/src/test/org/apache/solr/security/TestSha256AuthenticationProvider.java b/solr/core/src/test/org/apache/solr/security/TestSha256AuthenticationProvider.java new file mode 100644 index 00000000000..6f5ef0d5634 --- /dev/null +++ b/solr/core/src/test/org/apache/solr/security/TestSha256AuthenticationProvider.java @@ -0,0 +1,46 @@ +package org.apache.solr.security; + +/* + * 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. + */ + +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.solr.SolrTestCaseJ4; +import org.apache.solr.util.CommandOperation; + +public class TestSha256AuthenticationProvider extends SolrTestCaseJ4 { + public void testAuthenticate(){ + Sha256AuthenticationProvider zkAuthenticationProvider = new Sha256AuthenticationProvider(); + zkAuthenticationProvider.init(Collections.emptyMap()); + + String pwd = "My#$Password"; + String user = "noble"; + Map latestConf = new LinkedHashMap<>(); + Map params = Collections.singletonMap(user, pwd); + Map result = zkAuthenticationProvider.edit(latestConf, + Collections.singletonList(new CommandOperation("set-user",params ))); + zkAuthenticationProvider = new Sha256AuthenticationProvider(); + zkAuthenticationProvider.init(result); + + assertTrue(zkAuthenticationProvider.authenticate(user, pwd)); + assertFalse(zkAuthenticationProvider.authenticate(user, "WrongPassword")); + assertFalse(zkAuthenticationProvider.authenticate("unknownuser", "WrongPassword")); + + } +}