| /** |
| * Licensed to the Apache Software Foundation (ASF) under one or more |
| * contributor license agreements. See the NOTICE file distributed with |
| * this work for additional information regarding copyright ownership. |
| * The ASF licenses this file to You under the Apache License, Version 2.0 |
| * (the "License"); you may not use this file except in compliance with |
| * the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, software |
| * distributed under the License is distributed on an "AS IS" BASIS, |
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| * See the License for the specific language governing permissions and |
| * limitations under the License. |
| */ |
| package org.apache.solr.mcf; |
| |
| import org.apache.lucene.index.*; |
| import org.apache.lucene.search.*; |
| import org.apache.lucene.queryParser.ParseException; |
| import org.apache.solr.search.QParserPlugin; |
| import org.apache.solr.search.QParser; |
| import org.apache.solr.common.params.CommonParams; |
| import org.apache.solr.common.params.MapSolrParams; |
| import org.apache.solr.common.params.SolrParams; |
| import org.apache.solr.common.util.NamedList; |
| import org.apache.solr.common.util.StrUtils; |
| import org.apache.solr.request.SolrQueryRequest; |
| import org.apache.solr.common.SolrException; |
| import org.apache.solr.common.params.CommonParams; |
| import org.apache.solr.common.params.ShardParams; |
| import org.apache.solr.common.params.SolrParams; |
| import org.apache.solr.common.util.NamedList; |
| import org.apache.solr.handler.component.ResponseBuilder; |
| import org.apache.solr.handler.component.SearchComponent; |
| import org.apache.solr.core.CloseHook; |
| import org.apache.solr.util.plugin.SolrCoreAware; |
| import org.apache.solr.core.SolrCore; |
| import org.apache.commons.httpclient.*; |
| import org.apache.commons.httpclient.methods.*; |
| import org.apache.commons.httpclient.params.*; |
| import org.slf4j.*; |
| |
| import java.io.*; |
| import java.util.*; |
| import java.net.*; |
| |
| /** |
| * Query parser plugin for ManifoldCF-specific document-level access control. |
| */ |
| public class ManifoldCFQParserPlugin extends QParserPlugin |
| { |
| /** The component name */ |
| static final public String COMPONENT_NAME = "mcf"; |
| /** The parameter that is supposed to contain the authenticated user name, possibly including the AD domain */ |
| static final public String AUTHENTICATED_USER_NAME = "AuthenticatedUserName"; |
| /** The parameter that is supposed to contain the MCF authorization domain, if any */ |
| static final public String AUTHENTICATED_USER_DOMAIN = "AuthenticatedUserDomain"; |
| /** If there are more than one user/domain, this prefix will allow us to get the users... */ |
| static final public String AUTHENTICATED_USER_NAME_PREFIX = "AuthenticatedUserName_"; |
| /** If there are more than one user/domain, this prefix will allow us to get the authorization domains... */ |
| static final public String AUTHENTICATED_USER_DOMAIN_PREFIX = "AuthenticatedUserDomain_"; |
| |
| /** This parameter is an array of strings, which contain the tokens to use if there is no authenticated user name. |
| * It's meant to work with mod_authz_annotate, |
| * running under Apache */ |
| static final public String USER_TOKENS = "UserTokens"; |
| |
| /** Special token for null security fields */ |
| static final public String NOSECURITY_TOKEN = "__nosecurity__"; |
| |
| /** The queries that we will not attempt to interfere with */ |
| static final private String[] globalAllowed = { "solrpingquery" }; |
| |
| /** A logger we can use */ |
| private static final Logger LOG = LoggerFactory.getLogger(ManifoldCFQParserPlugin.class); |
| |
| // Member variables |
| String authorityBaseURL = null; |
| String fieldAllowDocument = null; |
| String fieldDenyDocument = null; |
| String fieldAllowShare = null; |
| String fieldDenyShare = null; |
| String fieldAllowParent = null; |
| String fieldDenyParent = null; |
| int socketTimeOut; |
| Integer connectionManagerSynchronizer = new Integer(0); |
| MultiThreadedHttpConnectionManager httpConnectionManager = null; |
| HttpClient client = null; |
| int poolSize; |
| |
| public ManifoldCFQParserPlugin() |
| { |
| super(); |
| } |
| |
| public void init(NamedList args) |
| { |
| authorityBaseURL = (String)args.get("AuthorityServiceBaseURL"); |
| if (authorityBaseURL == null) |
| authorityBaseURL = "http://localhost:8345/mcf-authority-service"; |
| Integer timeOut = (Integer)args.get("SocketTimeOut"); |
| socketTimeOut = timeOut == null ? 300000 : timeOut; |
| String allowAttributePrefix = (String)args.get("AllowAttributePrefix"); |
| String denyAttributePrefix = (String)args.get("DenyAttributePrefix"); |
| if (allowAttributePrefix == null) |
| allowAttributePrefix = "allow_token_"; |
| if (denyAttributePrefix == null) |
| denyAttributePrefix = "deny_token_"; |
| fieldAllowDocument = allowAttributePrefix+"document"; |
| fieldDenyDocument = denyAttributePrefix+"document"; |
| fieldAllowShare = allowAttributePrefix+"share"; |
| fieldDenyShare = denyAttributePrefix+"share"; |
| fieldAllowParent = allowAttributePrefix+"parent"; |
| fieldDenyParent = denyAttributePrefix+"parent"; |
| Integer connectionPoolSize = (Integer)args.get("ConnectionPoolSize"); |
| poolSize = (connectionPoolSize==null)?50:connectionPoolSize.intValue(); |
| } |
| |
| protected void initializeClient(SolrCore core) |
| { |
| synchronized (connectionManagerSynchronizer) |
| { |
| if (client == null) |
| { |
| // Initialize the connection pool |
| HttpConnectionManagerParams params = new HttpConnectionManagerParams(); |
| params.setTcpNoDelay(true); |
| params.setStaleCheckingEnabled(false); |
| params.setDefaultMaxConnectionsPerHost(poolSize); |
| params.setMaxTotalConnections(poolSize); |
| httpConnectionManager = new MultiThreadedHttpConnectionManager(); |
| httpConnectionManager.setParams(params); |
| client = new HttpClient(httpConnectionManager); |
| core.addCloseHook(new CloseHandler()); |
| } |
| } |
| } |
| |
| @Override |
| public QParser createParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) |
| { |
| initializeClient(req.getCore()); |
| return new ManifoldCFQueryParser(qstr,localParams,params,req); |
| } |
| |
| protected class ManifoldCFQueryParser extends QParser |
| { |
| public ManifoldCFQueryParser(String qstr, SolrParams localParams, SolrParams params, SolrQueryRequest req) |
| { |
| super(qstr,localParams,params,req); |
| } |
| |
| @Override |
| /** Create and return the <code>Query</code> object represented by <code>qstr</code> |
| * @see #getQuery() |
| **/ |
| public Query parse() throws ParseException |
| { |
| SolrParams params = req.getParams(); |
| |
| List<String> userAccessTokens; |
| |
| // Map from domain to user |
| Map<String,String> domainMap = new HashMap<String,String>(); |
| |
| // Get the authenticated user name from the parameters |
| String authenticatedUserName = params.get(AUTHENTICATED_USER_NAME); |
| if (authenticatedUserName != null) |
| { |
| String authenticatedUserDomain = params.get(AUTHENTICATED_USER_DOMAIN); |
| if (authenticatedUserDomain == null) |
| authenticatedUserDomain = ""; |
| domainMap.put(authenticatedUserDomain, authenticatedUserName); |
| } |
| else |
| { |
| // Look for user names/domains using the prefix |
| int i = 0; |
| while (true) |
| { |
| String userName = params.get(AUTHENTICATED_USER_NAME_PREFIX+i); |
| String domain = params.get(AUTHENTICATED_USER_DOMAIN+i); |
| if (userName == null) |
| break; |
| if (domain == null) |
| domain = ""; |
| domainMap.put(domain,userName); |
| i++; |
| } |
| } |
| |
| // If this parameter is empty or does not exist, we have to presume this is a guest, and treat them accordingly |
| if (domainMap.size() == 0) |
| { |
| // No authenticated user name. |
| // mod_authz_annotate may be in use upstream, so look for tokens from it. |
| userAccessTokens = new ArrayList<String>(); |
| String[] passedTokens = params.getParams(USER_TOKENS); |
| if (passedTokens == null) |
| { |
| // Only return 'public' documents (those with no security tokens at all) |
| LOG.info("Default no-user response (open documents only)"); |
| } |
| else |
| { |
| // Only return 'public' documents (those with no security tokens at all) |
| LOG.info("Group tokens received from caller"); |
| for (String passedToken : passedTokens) |
| { |
| userAccessTokens.add(passedToken); |
| } |
| } |
| } |
| else |
| { |
| 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()+"'"); |
| // Valid authenticated user name. Look up access tokens for the user. |
| // Check the configuration arguments for validity |
| if (authorityBaseURL == null) |
| { |
| throw new SolrException(SolrException.ErrorCode.SERVER_ERROR, "Error initializing ManifoldCFSecurityFilter component: 'AuthorityServiceBaseURL' init parameter required"); |
| } |
| try |
| { |
| userAccessTokens = getAccessTokens(domainMap); |
| } |
| catch (IOException e) |
| { |
| LOG.error("IO exception communicating with MCF authority service: "+e.getMessage(),e); |
| throw new ParseException("IO exception communicating with MCF authority service: "+e.getMessage()); |
| } |
| } |
| |
| BooleanQuery bq = new BooleanQuery(); |
| //bf.setMaxClauseCount(100000); |
| |
| Query allowShareOpen = new TermQuery(new Term(fieldAllowShare,NOSECURITY_TOKEN)); |
| Query denyShareOpen = new TermQuery(new Term(fieldDenyShare,NOSECURITY_TOKEN)); |
| |
| Query allowParentOpen = new TermQuery(new Term(fieldAllowParent,NOSECURITY_TOKEN)); |
| Query denyParentOpen = new TermQuery(new Term(fieldDenyParent,NOSECURITY_TOKEN)); |
| |
| Query allowDocumentOpen = new TermQuery(new Term(fieldAllowDocument,NOSECURITY_TOKEN)); |
| Query denyDocumentOpen = new TermQuery(new Term(fieldDenyDocument,NOSECURITY_TOKEN)); |
| |
| if (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.add(allowShareOpen,BooleanClause.Occur.MUST); |
| bq.add(denyShareOpen,BooleanClause.Occur.MUST); |
| bq.add(allowParentOpen,BooleanClause.Occur.MUST); |
| bq.add(denyParentOpen,BooleanClause.Occur.MUST); |
| bq.add(allowDocumentOpen,BooleanClause.Occur.MUST); |
| bq.add(denyDocumentOpen,BooleanClause.Occur.MUST); |
| } |
| else |
| { |
| // Extend the query appropriately for each user access token. |
| bq.add(calculateCompleteSubquery(fieldAllowShare,fieldDenyShare,allowShareOpen,denyShareOpen,userAccessTokens), |
| BooleanClause.Occur.MUST); |
| bq.add(calculateCompleteSubquery(fieldAllowParent,fieldDenyParent,allowParentOpen,denyParentOpen,userAccessTokens), |
| BooleanClause.Occur.MUST); |
| bq.add(calculateCompleteSubquery(fieldAllowDocument,fieldDenyDocument,allowDocumentOpen,denyDocumentOpen,userAccessTokens), |
| BooleanClause.Occur.MUST); |
| } |
| |
| return new ConstantScoreQuery(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 Query calculateCompleteSubquery(String allowField, String denyField, Query allowOpen, Query denyOpen, List<String> userAccessTokens) |
| { |
| BooleanQuery bq = new BooleanQuery(); |
| bq.setMaxClauseCount(1000000); |
| |
| // Add the empty-acl case |
| BooleanQuery subUnprotectedClause = new BooleanQuery(); |
| subUnprotectedClause.add(allowOpen,BooleanClause.Occur.MUST); |
| subUnprotectedClause.add(denyOpen,BooleanClause.Occur.MUST); |
| bq.add(subUnprotectedClause,BooleanClause.Occur.SHOULD); |
| for (String accessToken : userAccessTokens) |
| { |
| bq.add(new TermQuery(new Term(allowField,accessToken)),BooleanClause.Occur.SHOULD); |
| bq.add(new TermQuery(new Term(denyField,accessToken)),BooleanClause.Occur.MUST_NOT); |
| } |
| return bq; |
| } |
| |
| // Protected methods |
| |
| /** Get access tokens given a username */ |
| protected List<String> getAccessTokens(Map<String,String> domainMap) |
| throws IOException |
| { |
| // We can make this more complicated later, with support for https etc., but this is enough to demonstrate how it all should work. |
| 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(); |
| |
| GetMethod method = new GetMethod(theURL); |
| try |
| { |
| method.getParams().setParameter("http.socket.timeout", socketTimeOut); |
| method.setFollowRedirects(true); |
| int rval = client.executeMethod(method); |
| if (rval != 200) |
| { |
| String response = method.getResponseBodyAsString(); |
| throw new SolrException(SolrException.ErrorCode.SERVER_ERROR,"Couldn't fetch user's access tokens from ManifoldCF authority service: "+Integer.toString(rval)+"; "+response); |
| } |
| InputStream is = method.getResponseBodyAsStream(); |
| try |
| { |
| Reader r = new InputStreamReader(is,method.getResponseCharSet()); |
| 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.releaseConnection(); |
| } |
| } |
| } |
| |
| /** CloseHook implementation. |
| */ |
| protected class CloseHandler extends CloseHook |
| { |
| public CloseHandler() |
| { |
| } |
| |
| @Override |
| public void preClose(SolrCore core) |
| { |
| } |
| |
| @Override |
| public void postClose(SolrCore core) |
| { |
| // Close the connection pool |
| if (httpConnectionManager != null) |
| { |
| httpConnectionManager.shutdown(); |
| httpConnectionManager = null; |
| client = null; |
| } |
| } |
| |
| } |
| |
| } |