mirror of https://github.com/apache/lucene.git
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:
parent
740b78de92
commit
bd430506ce
|
@ -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
|
||||
----------------------
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
|
@ -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");
|
||||
}
|
|
@ -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 {
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
|
@ -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'}}}}";
|
||||
}
|
|
@ -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"));
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue