blob: 59891c75870e810d4c6be5e11af7f2c12b34d0ad [file] [log] [blame]
/* $Id$ */
/**
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.manifoldcf.elasticsearch;
import java.io.*;
import java.util.*;
import java.net.*;
import org.elasticsearch.index.query.FilterBuilder;
import org.elasticsearch.index.query.BoolFilterBuilder;
import org.elasticsearch.index.query.TermFilterBuilder;
import org.elasticsearch.common.logging.Loggers;
import org.elasticsearch.common.logging.ESLogger;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.HttpResponse;
import org.apache.http.params.BasicHttpParams;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.util.EntityUtils;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.impl.client.DefaultRedirectStrategy;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.impl.conn.PoolingClientConnectionManager;
/** This class represents the main Java API for modifying SearchRequestBuilder
* objects within ElasticSearch. It is a singleton class whose main public method
* is thread-safe.
*/
public class MCFAuthorizer
{
/** Special token for null security fields */
static final public String NOSECURITY_TOKEN = "__nosecurity__";
/** A logger we can use */
private static final ESLogger LOG = Loggers.getLogger(MCFAuthorizer.class);
// Member variables
protected final String authorityBaseURL;
protected final String fieldAllowDocument;
protected final String fieldDenyDocument;
protected final String fieldAllowParent;
protected final String fieldDenyParent;
protected final String fieldAllowShare;
protected final String fieldDenyShare;
protected final int connectionTimeout;
protected final int socketTimeout;
protected final int poolSize;
protected final ClientConnectionManager connectionManager;
protected final HttpClient httpClient;
/** Constructor, which includes configuration information */
public MCFAuthorizer(MCFConfigurationParameters cp)
{
authorityBaseURL = cp.authorityServiceBaseURL;
fieldAllowDocument = cp.allowFieldPrefix+"document";
fieldDenyDocument = cp.denyFieldPrefix+"document";
fieldAllowShare = cp.allowFieldPrefix+"share";
fieldDenyShare = cp.denyFieldPrefix+"share";
fieldAllowParent = cp.allowFieldPrefix+"parent";
fieldDenyParent = cp.denyFieldPrefix+"parent";
connectionTimeout = cp.connectionTimeout;
socketTimeout = cp.socketTimeout;
poolSize = cp.connectionPoolSize;
// Set up client pool etc, if there's indication that we should do that
if (authorityBaseURL != null)
{
PoolingClientConnectionManager localConnectionManager = new PoolingClientConnectionManager();
localConnectionManager.setMaxTotal(poolSize);
localConnectionManager.setDefaultMaxPerRoute(poolSize);
connectionManager = localConnectionManager;
BasicHttpParams params = new BasicHttpParams();
params.setBooleanParameter(CoreConnectionPNames.TCP_NODELAY,true);
params.setBooleanParameter(CoreConnectionPNames.STALE_CONNECTION_CHECK,true);
params.setIntParameter(CoreConnectionPNames.SO_TIMEOUT,socketTimeout);
params.setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT,connectionTimeout);
DefaultHttpClient localClient = new DefaultHttpClient(connectionManager,params);
localClient.setRedirectStrategy(new DefaultRedirectStrategy());
httpClient = localClient;
}
else
{
connectionManager = null;
httpClient = null;
}
}
/** Shut down the pool etc.
*/
public void shutdown()
{
if (connectionManager != null)
connectionManager.shutdown();
}
/** Main method for building a filter representing appropriate security.
*@param authenticatedUserNamesAndDomains is a list of user names and its domains in the form "user:mcfdomain".
*@return the filter builder.
*/
public FilterBuilder buildAuthorizationFilter(String[] authenticatedUserNamesAndDomains)
throws MCFAuthorizerException{
Map<String,String> domainMap = new HashMap<String,String>();
for(String buffer : authenticatedUserNamesAndDomains){
String[] authenticatedUserNameAndDomain = buffer.split(":", 2);
String authenticatedUserName = authenticatedUserNameAndDomain[0];
String authenticatedUserDomain;
if(authenticatedUserNameAndDomain.length<2) authenticatedUserDomain="";
else authenticatedUserDomain=authenticatedUserNameAndDomain[1];
domainMap.put(authenticatedUserDomain, authenticatedUserName);
}
return buildAuthorizationFilter(domainMap);
}
/** Main method for building a filter representing appropriate security.
*@param domainMap is a map from MCF authorization domain name to user name,
* and describes a complete user identity.
*@return the filter builder.
*/
public FilterBuilder buildAuthorizationFilter(Map<String,String> domainMap)
throws MCFAuthorizerException
{
if (authorityBaseURL == null)
throw new IllegalStateException("Authority base URL required for finding access tokens for a user");
if (domainMap == null || domainMap.size() == 0)
throw new IllegalArgumentException("Cannot find user tokens for null user");
if(LOG.isInfoEnabled()){
StringBuilder sb = new StringBuilder("[");
boolean first = true;
for (String domain : domainMap.keySet())
{
if (!first)
sb.append(",");
else
first = false;
sb.append(domain).append(":").append(domainMap.get(domain));
}
sb.append("]");
LOG.info("Trying to match docs for user '"+sb.toString()+"'");
}
return buildAuthorizationFilter(getAccessTokens(domainMap));
}
/** Main method for building a filter representing appropriate security.
*@param authenticatedUserName is a user name in the form "user:mcfdomain".
*@return the filter builder.
*/
public FilterBuilder buildAuthorizationFilter(String authenticatedUserName)
throws MCFAuthorizerException
{
return buildAuthorizationFilter(authenticatedUserName, "");
}
/** Main method for building a filter representing appropriate security.
*@param authenticatedUserName is a user name in the form "user".
*@param authenticatedUserDomain is the corresponding MCF authorization domain.
*@return the filter builder.
*/
public FilterBuilder buildAuthorizationFilter(String authenticatedUserName, String authenticatedUserDomain)
throws MCFAuthorizerException
{
Map<String,String> domainMap = new HashMap<String,String>();
domainMap.put(authenticatedUserDomain, authenticatedUserName);
return buildAuthorizationFilter(domainMap);
}
/** Main method for building a filter representing appropriate security.
*@param userAccessTokens are a set of tokens to use to construct the filter (presumably from mod_authz_annotate, upstream)
*@return the wrapped query enforcing ManifoldCF security.
*/
public FilterBuilder buildAuthorizationFilter(List<String> userAccessTokens)
throws MCFAuthorizerException
{
BoolFilterBuilder bq = new BoolFilterBuilder();
FilterBuilder allowShareOpen = new TermFilterBuilder(fieldAllowShare,NOSECURITY_TOKEN);
FilterBuilder denyShareOpen = new TermFilterBuilder(fieldDenyShare,NOSECURITY_TOKEN);
FilterBuilder allowParentOpen = new TermFilterBuilder(fieldAllowParent,NOSECURITY_TOKEN);
FilterBuilder denyParentOpen = new TermFilterBuilder(fieldDenyParent,NOSECURITY_TOKEN);
FilterBuilder allowDocumentOpen = new TermFilterBuilder(fieldAllowDocument,NOSECURITY_TOKEN);
FilterBuilder denyDocumentOpen = new TermFilterBuilder(fieldDenyDocument,NOSECURITY_TOKEN);
if (userAccessTokens == null || userAccessTokens.size() == 0)
{
// Only open documents can be included.
// That query is:
// (fieldAllowShare is empty AND fieldDenyShare is empty AND fieldAllowDocument is empty AND fieldDenyDocument is empty)
// We're trying to map to: -(fieldAllowShare:*) , which should be pretty efficient in Solr because it is negated. If this turns out not to be so, then we should
// have the SolrConnector inject a special token into these fields when they otherwise would be empty, and we can trivially match on that token.
bq.must(allowShareOpen);
bq.must(denyShareOpen);
bq.must(allowParentOpen);
bq.must(denyParentOpen);
bq.must(allowDocumentOpen);
bq.must(denyDocumentOpen);
}
else
{
// Extend the query appropriately for each user access token.
bq.must(calculateCompleteSubquery(fieldAllowShare,fieldDenyShare,allowShareOpen,denyShareOpen,userAccessTokens));
bq.must(calculateCompleteSubquery(fieldAllowDocument,fieldDenyDocument,allowDocumentOpen,denyDocumentOpen,userAccessTokens));
bq.must(calculateCompleteSubquery(fieldAllowParent,fieldDenyParent,allowParentOpen,denyParentOpen,userAccessTokens));
}
return bq;
}
/** Calculate a complete subclause, representing something like:
* ((fieldAllowShare is empty AND fieldDenyShare is empty) OR fieldAllowShare HAS token1 OR fieldAllowShare HAS token2 ...)
* AND fieldDenyShare DOESN'T_HAVE token1 AND fieldDenyShare DOESN'T_HAVE token2 ...
*/
protected static FilterBuilder calculateCompleteSubquery(String allowField, String denyField, FilterBuilder allowOpen, FilterBuilder denyOpen, List<String> userAccessTokens)
{
BoolFilterBuilder bq = new BoolFilterBuilder();
// No ES equivalent - hope this is done right inside
//bq.setMaxClauseCount(1000000);
// Add the empty-acl case
BoolFilterBuilder subUnprotectedClause = new BoolFilterBuilder();
subUnprotectedClause.must(allowOpen);
subUnprotectedClause.must(denyOpen);
bq.should(subUnprotectedClause);
for (String accessToken : userAccessTokens)
{
bq.should(new TermFilterBuilder(allowField,accessToken));
bq.mustNot(new TermFilterBuilder(denyField,accessToken));
}
return bq;
}
/** Get access tokens given a username */
protected List<String> getAccessTokens(Map<String,String> domainMap)
throws MCFAuthorizerException
{
try
{
StringBuilder urlBuffer = new StringBuilder(authorityBaseURL);
urlBuffer.append("/UserACLs");
int i = 0;
for (String domain : domainMap.keySet())
{
if (i == 0)
urlBuffer.append("?");
else
urlBuffer.append("&");
// For backwards compatibility, handle the singleton case specially
if (domainMap.size() == 1 && domain.length() == 0)
{
urlBuffer.append("username=").append(URLEncoder.encode(domainMap.get(domain),"utf-8"));
}
else
{
urlBuffer.append("username_").append(Integer.toString(i)).append("=").append(URLEncoder.encode(domainMap.get(domain),"utf-8")).append("&")
.append("domain_").append(Integer.toString(i)).append("=").append(URLEncoder.encode(domain,"utf-8"));
}
i++;
}
String theURL = urlBuffer.toString();
HttpGet method = new HttpGet(theURL);
try
{
HttpResponse httpResponse = httpClient.execute(method);
int rval = httpResponse.getStatusLine().getStatusCode();
if (rval != 200)
{
String response = EntityUtils.toString(httpResponse.getEntity(),"utf-8");
throw new MCFAuthorizerException("Couldn't fetch user's access tokens from ManifoldCF authority service: "+Integer.toString(rval)+"; "+response);
}
InputStream is = httpResponse.getEntity().getContent();
try
{
String charSet = EntityUtils.getContentCharSet(httpResponse.getEntity());
if (charSet == null)
charSet = "utf-8";
Reader r = new InputStreamReader(is,charSet);
try
{
BufferedReader br = new BufferedReader(r);
try
{
// Read the tokens, one line at a time. If any authorities are down, we have no current way to note that, but someday we will.
List<String> tokenList = new ArrayList<String>();
while (true)
{
String line = br.readLine();
if (line == null)
break;
if (line.startsWith("TOKEN:"))
{
tokenList.add(line.substring("TOKEN:".length()));
}
else
{
// It probably says something about the state of the authority(s) involved, so log it
LOG.info("Saw authority response "+line);
}
}
return tokenList;
}
finally
{
br.close();
}
}
finally
{
r.close();
}
}
finally
{
is.close();
}
}
finally
{
method.abort();
}
}
catch (IOException e)
{
throw new MCFAuthorizerException("IO exception: "+e.getMessage(),e);
}
}
}