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
This commit is contained in:
Noble Paul 2015-08-06 19:03:25 +00:00
parent 740b78de92
commit bd430506ce
7 changed files with 809 additions and 8 deletions

View File

@ -201,6 +201,9 @@ New Features
* SOLR-7838: An authorizationPlugin interface where the access control rules are stored/managed in * SOLR-7838: An authorizationPlugin interface where the access control rules are stored/managed in
ZooKeeper (Noble Paul, Anshum Gupta, Ishan Chattopadhyaya) 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 Bug Fixes
---------------------- ----------------------

View File

@ -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<Header> authHeader = new ThreadLocal<>();
public boolean authenticate(String username, String pwd) {
return zkAuthentication.authenticate(username, pwd);
}
@Override
public void init(Map<String, Object> pluginConfig) {
zkAuthentication = getAuthenticationProvider(pluginConfig);
}
@Override
public Map<String, Object> edit(Map<String, Object> latestConf, List<CommandOperation> 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<String, Object> pluginConfig) {
Sha256AuthenticationProvider provider = new Sha256AuthenticationProvider();
provider.init(pluginConfig);
return provider;
}
private void authenticationFailure(HttpServletResponse response, String message) throws IOException {
for (Map.Entry<String, String> 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<String, Object> pluginConfig);
boolean authenticate(String user, String pwd);
Map<String, String> getPromptHeaders();
}
}

View File

@ -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<String, String> credentials;
private String realm;
private Map<String, String> 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<String, Object> 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<String,String> users = (Map<String,String>) pluginConfig.get("credentials");
if (users == null) {
BasicAuthPlugin.log.warn("No users configured yet");
return;
}
for (Map.Entry<String, String> 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<String, String> 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<String, Object> edit(Map<String, Object> latestConf, List<CommandOperation> 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<String> 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<String> supported_ops = ImmutableSet.of("set-user", "delete-user");
}

View File

@ -17,6 +17,14 @@ package org.apache.solr.cloud;
* limitations under the License. * 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 com.carrotsearch.randomizedtesting.rules.SystemPropertiesRestoreRule;
import org.apache.lucene.util.LuceneTestCase; import org.apache.lucene.util.LuceneTestCase;
import org.apache.lucene.util.LuceneTestCase.SuppressSysoutChecks; import org.apache.lucene.util.LuceneTestCase.SuppressSysoutChecks;
@ -45,14 +53,6 @@ import org.junit.rules.TestRule;
import org.slf4j.Logger; import org.slf4j.Logger;
import org.slf4j.LoggerFactory; 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, * Test of the MiniSolrCloudCluster functionality. Keep in mind,
* MiniSolrCloudCluster is designed to be used outside of the Lucene test * 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); assertTrue(e.code() >= 500 && e.code() < 600);
} }
doExtraTests(miniCluster, zkClient, zkStateReader,cloudSolrClient, collectionName);
// delete the collection we created earlier // delete the collection we created earlier
miniCluster.deleteCollection(collectionName); miniCluster.deleteCollection(collectionName);
AbstractDistribZkTestBase.waitForCollectionToDisappear(collectionName, zkStateReader, true, true, 330); 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 @Test
public void testErrorsInStartup() throws Exception { public void testErrorsInStartup() throws Exception {

View File

@ -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<String, Object>) 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<String, Object> 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<String, Object> 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<String, Object>) 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<String, Object> 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());
}
}

View File

@ -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<Object> 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<String> 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<Replica> 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'}}}}";
}

View File

@ -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<String, Object> params = Collections.singletonMap(user, pwd);
Map<String, Object> 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"));
}
}