/* * Copyright 2013 The Apache Software Foundation. * * Licensed 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.authorities.authorities.generic; import java.io.IOException; import java.net.MalformedURLException; import java.net.URL; import org.apache.manifoldcf.core.util.URLEncoder; import java.nio.charset.StandardCharsets; import java.util.List; import java.util.Locale; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Unmarshaller; import org.apache.http.HttpException; import org.apache.http.HttpRequest; import org.apache.http.HttpRequestInterceptor; import org.apache.http.HttpResponse; import org.apache.http.HttpStatus; import org.apache.http.auth.AuthScope; import org.apache.http.auth.Credentials; import org.apache.http.auth.UsernamePasswordCredentials; import org.apache.http.client.HttpClient; import org.apache.http.client.methods.HttpGet; import org.apache.http.impl.auth.BasicScheme; import org.apache.http.impl.client.DefaultHttpClient; import org.apache.http.params.HttpConnectionParams; import org.apache.http.protocol.HttpContext; import org.apache.http.util.EntityUtils; import org.apache.manifoldcf.authorities.interfaces.AuthorizationResponse; import org.apache.manifoldcf.authorities.system.ManifoldCF; import org.apache.manifoldcf.core.interfaces.CacheManagerFactory; import org.apache.manifoldcf.core.interfaces.ConfigParams; import org.apache.manifoldcf.core.interfaces.ICacheCreateHandle; import org.apache.manifoldcf.core.interfaces.ICacheDescription; import org.apache.manifoldcf.core.interfaces.ICacheHandle; import org.apache.manifoldcf.core.interfaces.ICacheManager; import org.apache.manifoldcf.core.interfaces.IHTTPOutput; import org.apache.manifoldcf.core.interfaces.IPostParameters; import org.apache.manifoldcf.core.interfaces.IThreadContext; import org.apache.manifoldcf.core.interfaces.ManifoldCFException; import org.apache.manifoldcf.core.interfaces.StringSet; import org.apache.manifoldcf.crawler.connectors.generic.api.Auth; import org.apache.manifoldcf.ui.util.Encoder; /** * * @author krycek */ public class GenericAuthority extends org.apache.manifoldcf.authorities.authorities.BaseAuthorityConnector { public static final String _rcsid = "@(#)$Id: GenericAuthority.java 1496653 2013-06-25 22:05:04Z mlizewski $"; /** * This is the active directory global deny token. This should be ingested * with all documents. */ private static final String globalDenyToken = "DEAD_AUTHORITY"; private static final AuthorizationResponse unreachableResponse = new AuthorizationResponse(new String[]{globalDenyToken}, AuthorizationResponse.RESPONSE_UNREACHABLE); private static final AuthorizationResponse userNotFoundResponse = new AuthorizationResponse(new String[]{globalDenyToken}, AuthorizationResponse.RESPONSE_USERNOTFOUND); private final static String ACTION_PARAM_NAME = "action"; private final static String ACTION_AUTH = "auth"; private final static String ACTION_CHECK = "check"; private String genericLogin = null; private String genericPassword = null; private String genericEntryPoint = null; private int connectionTimeoutMillis = 60 * 1000; private int socketTimeoutMillis = 30 * 60 * 1000; private long responseLifetime = 60000L; //60sec private int LRUsize = 1000; private DefaultHttpClient client = null; private long sessionExpirationTime = -1L; /** * Cache manager. */ private ICacheManager cacheManager = null; /** * Constructor. */ public GenericAuthority() { } /** * Set thread context. */ @Override public void setThreadContext(IThreadContext tc) throws ManifoldCFException { super.setThreadContext(tc); cacheManager = CacheManagerFactory.make(tc); } /** * Connect. The configuration parameters are included. * * @param configParams are the configuration parameters for this connection. */ @Override public void connect(ConfigParams configParams) { super.connect(configParams); genericEntryPoint = getParam(configParams, "genericEntryPoint", null); genericLogin = getParam(configParams, "genericLogin", null); genericPassword = ""; try { genericPassword = ManifoldCF.deobfuscate(getParam(configParams, "genericPassword", "")); } catch (ManifoldCFException ignore) { } connectionTimeoutMillis = Integer.parseInt(getParam(configParams, "genericConnectionTimeout", "60000")); if (connectionTimeoutMillis == 0) { connectionTimeoutMillis = 60000; } socketTimeoutMillis = Integer.parseInt(getParam(configParams, "genericSocketTimeout", "1800000")); if (socketTimeoutMillis == 0) { socketTimeoutMillis = 1800000; } responseLifetime = Long.parseLong(getParam(configParams, "genericResponseLifetime", "60000")); if (responseLifetime == 0) { responseLifetime = 60000; } } protected DefaultHttpClient getClient() throws ManifoldCFException { synchronized (this) { if (client != null) { return client; } DefaultHttpClient cl = new DefaultHttpClient(); if (genericLogin != null && !genericLogin.isEmpty()) { try { URL url = new URL(genericEntryPoint); Credentials credentials = new UsernamePasswordCredentials(genericLogin, genericPassword); cl.getCredentialsProvider().setCredentials(new AuthScope(url.getHost(), url.getPort() > 0 ? url.getPort() : 80, AuthScope.ANY_REALM), credentials); cl.addRequestInterceptor(new PreemptiveAuth(credentials), 0); } catch (MalformedURLException ex) { client = null; sessionExpirationTime = -1L; throw new ManifoldCFException("getClient exception: " + ex.getMessage(), ex); } } HttpConnectionParams.setConnectionTimeout(cl.getParams(), connectionTimeoutMillis); HttpConnectionParams.setSoTimeout(cl.getParams(), socketTimeoutMillis); sessionExpirationTime = System.currentTimeMillis() + 300000L; client = cl; return cl; } } /** * Poll. The connection should be closed if it has been idle for too long. */ @Override public void poll() throws ManifoldCFException { if (client != null && System.currentTimeMillis() > sessionExpirationTime) { disconnectSession(); } super.poll(); } /** This method is called to assess whether to count this connector instance should * actually be counted as being connected. *@return true if the connector instance is actually connected. */ @Override public boolean isConnected() { return client != null; } /** * Check connection for sanity. */ @Override public String check() throws ManifoldCFException { HttpClient client = getClient(); try { CheckThread checkThread = new CheckThread(client, genericEntryPoint + "?" + ACTION_PARAM_NAME + "=" + ACTION_CHECK); checkThread.start(); checkThread.join(); if (checkThread.getException() != null) { Throwable thr = checkThread.getException(); return "Check exception: " + thr.getMessage(); } return checkThread.getResult(); } catch (InterruptedException ex) { throw new ManifoldCFException(ex.getMessage(), ex, ManifoldCFException.INTERRUPTED); } } /** * Close the connection. Call this before discarding the repository connector. */ @Override public void disconnect() throws ManifoldCFException { disconnectSession(); super.disconnect(); // Zero out all the stuff that we want to be sure we don't use again genericEntryPoint = null; genericLogin = null; genericPassword = null; } protected String createCacheConnectionString() { StringBuilder sb = new StringBuilder(); sb.append(genericEntryPoint).append("#").append(genericLogin); return sb.toString(); } /** * Obtain the access tokens for a given user name. * * @param userName is the user name or identifier. * @return the response tokens (according to the current authority). (Should * throws an exception only when a condition cannot be properly described * within the authorization response object.) */ @Override public AuthorizationResponse getAuthorizationResponse(String userName) throws ManifoldCFException { HttpClient client = getClient(); // Construct a cache description object ICacheDescription objectDescription = new GenericAuthorizationResponseDescription(userName, createCacheConnectionString(), this.responseLifetime, this.LRUsize); // Enter the cache ICacheHandle ch = cacheManager.enterCache(new ICacheDescription[]{objectDescription}, null, null); try { ICacheCreateHandle createHandle = cacheManager.enterCreateSection(ch); try { // Lookup the object AuthorizationResponse response = (AuthorizationResponse) cacheManager.lookupObject(createHandle, objectDescription); if (response != null) { return response; } // Create the object. response = getAuthorizationResponseUncached(client, userName); // Save it in the cache cacheManager.saveObject(createHandle, objectDescription, response); // And return it... return response; } finally { cacheManager.leaveCreateSection(createHandle); } } finally { cacheManager.leaveCache(ch); } } protected AuthorizationResponse getAuthorizationResponseUncached(HttpClient client, String userName) throws ManifoldCFException { StringBuilder url = new StringBuilder(genericEntryPoint); url.append("?").append(ACTION_PARAM_NAME).append("=").append(ACTION_AUTH); url.append("&username=").append(URLEncoder.encode(userName)); try { FetchTokensThread t = new FetchTokensThread(client, url.toString()); t.start(); t.join(); if (t.getException() != null) { return unreachableResponse; } Auth auth = t.getAuthResponse(); if (auth == null) { return userNotFoundResponse; } if (!auth.exists) { return userNotFoundResponse; } if (auth.tokens == null) { return new AuthorizationResponse(new String[]{}, AuthorizationResponse.RESPONSE_OK); } String[] tokens = new String[auth.tokens.size()]; int k = 0; while (k < tokens.length) { tokens[k] = (String) auth.tokens.get(k); k++; } return new AuthorizationResponse(tokens, AuthorizationResponse.RESPONSE_OK); } catch (InterruptedException ex) { throw new ManifoldCFException(ex.getMessage(), ex, ManifoldCFException.INTERRUPTED); } } /** * Obtain the default access tokens for a given user name. * * @param userName is the user name or identifier. * @return the default response tokens, presuming that the connect method * fails. */ @Override public AuthorizationResponse getDefaultAuthorizationResponse(String userName) { // The default response if the getConnection method fails return unreachableResponse; } // UI support methods. // // These support methods are involved in setting up authority connection configuration information. The configuration methods cannot assume that the // current authority object is connected. That is why they receive a thread context argument. @Override public void outputConfigurationHeader(IThreadContext threadContext, IHTTPOutput out, Locale locale, ConfigParams parameters, List<String> tabsArray) throws ManifoldCFException, IOException { tabsArray.add(Messages.getString(locale, "generic.EntryPoint")); out.print( "<script type=\"text/javascript\">\n" + "<!--\n" + "function checkConfig() {\n" + " return true;\n" + "}\n" + "\n" + "function checkConfigForSave() {\n" + " if (editconnection.genericEntryPoint.value == \"\") {\n" + " alert(\"" + Messages.getBodyJavascriptString(locale, "generic.EntryPointCannotBeBlank") + "\");\n" + " SelectTab(\"" + Messages.getBodyJavascriptString(locale, "generic.EntryPoint") + "\");\n" + " editconnection.genericEntryPoint.focus();\n" + " return false;\n" + " }\n" + " return true;\n" + "}\n" + "//-->\n" + "</script>\n"); } @Override public void outputConfigurationBody(IThreadContext threadContext, IHTTPOutput out, Locale locale, ConfigParams parameters, String tabName) throws ManifoldCFException, IOException { String server = getParam(parameters, "genericEntryPoint", ""); String login = getParam(parameters, "genericLogin", ""); String password = ""; try { password = out.mapPasswordToKey(ManifoldCF.deobfuscate(getParam(parameters, "genericPassword", ""))); } catch (ManifoldCFException ignore) { } String conTimeout = getParam(parameters, "genericConnectionTimeout", "60000"); String soTimeout = getParam(parameters, "genericSocketTimeout", "1800000"); String respLifetime = getParam(parameters, "genericResponseLifetime", "60000"); if (tabName.equals(Messages.getString(locale, "generic.EntryPoint"))) { out.print( "<table class=\"displaytable\">\n" + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.EntryPointColon") + "</nobr></td>\n" + " <td class=\"value\"><input type=\"text\" size=\"32\" name=\"genericEntryPoint\" value=\"" + Encoder.attributeEscape(server) + "\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.LoginColon") + "</nobr></td>\n" + " <td class=\"value\"><input type=\"text\" size=\"32\" name=\"genericLogin\" value=\"" + Encoder.attributeEscape(login) + "\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.PasswordColon") + "</nobr></td>\n" + " <td class=\"value\"><input type=\"password\" size=\"32\" name=\"genericPassword\" value=\"" + Encoder.attributeEscape(password) + "\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.ConnectionTimeoutColon") + "</nobr></td>\n" + " <td class=\"value\"><input type=\"text\" size=\"32\" name=\"genericConTimeout\" value=\"" + Encoder.attributeEscape(conTimeout) + "\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.SocketTimeoutColon") + "</nobr></td>\n" + " <td class=\"value\"><input type=\"text\" size=\"32\" name=\"genericSoTimeout\" value=\"" + Encoder.attributeEscape(soTimeout) + "\"/></td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.ResponseLifetimeColon") + "</nobr></td>\n" + " <td class=\"value\"><input type=\"text\" size=\"32\" name=\"genericResponseLifetime\" value=\"" + Encoder.attributeEscape(respLifetime) + "\"/></td>\n" + " </tr>\n" + "</table>\n"); } else { out.print("<input type=\"hidden\" name=\"genericEntryPoint\" value=\"" + Encoder.attributeEscape(server) + "\"/>\n"); out.print("<input type=\"hidden\" name=\"genericLogin\" value=\"" + Encoder.attributeEscape(login) + "\"/>\n"); out.print("<input type=\"hidden\" name=\"genericPassword\" value=\"" + Encoder.attributeEscape(password) + "\"/>\n"); out.print("<input type=\"hidden\" name=\"genericConTimeout\" value=\"" + Encoder.attributeEscape(conTimeout) + "\"/>\n"); out.print("<input type=\"hidden\" name=\"genericSoTimeout\" value=\"" + Encoder.attributeEscape(soTimeout) + "\"/>\n"); out.print("<input type=\"hidden\" name=\"genericResponseLifetime\" value=\"" + Encoder.attributeEscape(respLifetime) + "\"/>\n"); } } @Override public String processConfigurationPost(IThreadContext threadContext, IPostParameters variableContext, Locale locale, ConfigParams parameters) throws ManifoldCFException { copyParam(variableContext, parameters, "genericLogin"); copyParam(variableContext, parameters, "genericEntryPoint"); copyParam(variableContext, parameters, "genericConTimeout"); copyParam(variableContext, parameters, "genericSoTimeout"); copyParam(variableContext, parameters, "genericResponseLifetime"); String password = variableContext.getParameter("genericPassword"); if (password == null) { password = ""; } parameters.setParameter("genericPassword", org.apache.manifoldcf.core.system.ManifoldCF.obfuscate(variableContext.mapKeyToPassword(password))); return null; } @Override public void viewConfiguration(IThreadContext threadContext, IHTTPOutput out, Locale locale, ConfigParams parameters) throws ManifoldCFException, IOException { String login = getParam(parameters, "genericLogin", ""); String server = getParam(parameters, "genericEntryPoint", ""); String conTimeout = getParam(parameters, "genericConnectionTimeout", "60000"); String soTimeout = getParam(parameters, "genericSocketTimeout", "1800000"); String respLifetime = getParam(parameters, "genericResponseLifetime", "60000"); out.print( "<table class=\"displaytable\">\n" + " <tr><td class=\"separator\" colspan=\"2\"><hr/></td></tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.EntryPointColon") + "</nobr></td>\n" + " <td class=\"value\">" + Encoder.bodyEscape(server) + "</td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.LoginColon") + "</nobr></td>\n" + " <td class=\"value\">" + Encoder.bodyEscape(login) + "</td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.PasswordColon") + "</nobr></td>\n" + " <td class=\"value\">**********</td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.ConnectionTimeoutColon") + "</nobr></td>\n" + " <td class=\"value\">" + Encoder.bodyEscape(conTimeout) + "</td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.SocketTimeoutColon") + "</nobr></td>\n" + " <td class=\"value\">" + Encoder.bodyEscape(soTimeout) + "</td>\n" + " </tr>\n" + " <tr>\n" + " <td class=\"description\"><nobr>" + Messages.getBodyString(locale, "generic.ResponseLifetimeColon") + "</nobr></td>\n" + " <td class=\"value\">" + Encoder.bodyEscape(respLifetime) + "</td>\n" + " </tr>\n" + "</table>\n"); } private String getParam(ConfigParams parameters, String name, String def) { return parameters.getParameter(name) != null ? parameters.getParameter(name) : def; } private boolean copyParam(IPostParameters variableContext, ConfigParams parameters, String name) { String val = variableContext.getParameter(name); if (val == null) { return false; } parameters.setParameter(name, val); return true; } // Protected methods protected static StringSet emptyStringSet = new StringSet(); private void disconnectSession() { synchronized (this) { client.getConnectionManager().shutdown(); client = null; } } /** * This is the cache object descriptor for cached access tokens from this * connector. */ protected class GenericAuthorizationResponseDescription extends org.apache.manifoldcf.core.cachemanager.BaseDescription { /** * The user name */ protected String userName; /** * LDAP connection string with server name and base DN */ protected String connectionString; /** * The response lifetime */ protected long responseLifetime; /** * The expiration time */ protected long expirationTime = -1; /** * Constructor. */ public GenericAuthorizationResponseDescription(String userName, String connectionString, long responseLifetime, int LRUsize) { super("LDAPAuthority", LRUsize); this.userName = userName; this.connectionString = connectionString; this.responseLifetime = responseLifetime; } /** * Return the invalidation keys for this object. */ @Override public StringSet getObjectKeys() { return emptyStringSet; } /** * Get the critical section name, used for synchronizing the creation of the * object */ @Override public String getCriticalSectionName() { StringBuilder sb = new StringBuilder(getClass().getName()); sb.append("-").append(userName).append("-").append(connectionString); return sb.toString(); } /** * Return the object expiration interval */ @Override public long getObjectExpirationTime(long currentTime) { if (expirationTime == -1) { expirationTime = currentTime + responseLifetime; } return expirationTime; } @Override public int hashCode() { return userName.hashCode() + connectionString.hashCode(); } @Override public boolean equals(Object o) { if (!(o instanceof GenericAuthorizationResponseDescription)) { return false; } GenericAuthorizationResponseDescription ard = (GenericAuthorizationResponseDescription) o; if (!ard.userName.equals(userName)) { return false; } if (!ard.connectionString.equals(connectionString)) { return false; } return true; } } static class PreemptiveAuth implements HttpRequestInterceptor { private Credentials credentials; public PreemptiveAuth(Credentials creds) { this.credentials = creds; } @Override public void process(final HttpRequest request, final HttpContext context) throws HttpException, IOException { request.addHeader(new BasicScheme(StandardCharsets.US_ASCII).authenticate(credentials, request, context)); } } protected static class CheckThread extends Thread { protected HttpClient client; protected String url; protected Throwable exception = null; protected String result = "Unknown"; public CheckThread(HttpClient client, String url) { super(); setDaemon(true); this.client = client; this.url = url; } @Override public void run() { HttpGet method = new HttpGet(url); try { HttpResponse response = client.execute(method); try { if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { result = "Connection failed: " + response.getStatusLine().getReasonPhrase(); return; } EntityUtils.consume(response.getEntity()); result = "Connection OK"; } finally { EntityUtils.consume(response.getEntity()); method.releaseConnection(); } } catch (IOException ex) { exception = ex; } } public Throwable getException() { return exception; } public String getResult() { return result; } } protected static class FetchTokensThread extends Thread { protected HttpClient client; protected String url; protected Throwable exception = null; protected Auth auth; public FetchTokensThread(HttpClient client, String url) { super(); setDaemon(true); this.client = client; this.url = url; this.auth = null; } @Override public void run() { try { HttpGet method = new HttpGet(url.toString()); HttpResponse response = client.execute(method); try { if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { exception = new ManifoldCFException("FetchTokensThread error - interface returned incorrect return code for: " + url + " - " + response.getStatusLine().toString()); return; } JAXBContext context; context = JAXBContext.newInstance(Auth.class); Unmarshaller m = context.createUnmarshaller(); auth = (Auth) m.unmarshal(response.getEntity().getContent()); } catch (JAXBException ex) { exception = ex; } finally { EntityUtils.consume(response.getEntity()); method.releaseConnection(); } } catch (Exception ex) { exception = ex; } } public Throwable getException() { return exception; } public Auth getAuthResponse() { return auth; } } }